Add questionnaire status management with draft, archive, and recover options
This commit is contained in:
parent
b67eca0729
commit
43461bbb2b
18 changed files with 5925 additions and 964 deletions
|
|
@ -1,11 +1,5 @@
|
||||||
using System;
|
using System.ComponentModel;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Model
|
namespace Model
|
||||||
{
|
{
|
||||||
|
|
@ -28,5 +22,8 @@ namespace Model
|
||||||
public Questionnaire? Questionnaire { get; set; }
|
public Questionnaire? Questionnaire { get; set; }
|
||||||
|
|
||||||
public List<Answer> Answers { get; set; }
|
public List<Answer> Answers { get; set; }
|
||||||
|
|
||||||
|
public bool IsActive { get; set; } = true; // Default to active
|
||||||
|
public DateTime CreatedDate { get; set; } = DateTime.UtcNow; // Default to now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,28 @@ using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Model
|
namespace Model
|
||||||
{
|
{
|
||||||
|
|
||||||
public class Questionnaire
|
public class Questionnaire
|
||||||
{
|
{
|
||||||
public Questionnaire()
|
public Questionnaire()
|
||||||
{
|
{
|
||||||
Questions = new List<Question>();
|
Questions = new List<Question>();
|
||||||
|
|
||||||
|
// ADD THESE NEW LINES (with safe defaults):
|
||||||
|
Status = QuestionnaireStatus.Draft; // Default to Draft
|
||||||
|
CreatedDate = DateTime.UtcNow; // Default to now
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EXISTING PROPERTIES (keep as-is):
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public List<Question>? Questions { get; set; }
|
public List<Question>? Questions { get; set; }
|
||||||
|
|
||||||
|
// ADD THESE NEW PROPERTIES:
|
||||||
|
public QuestionnaireStatus Status { get; set; } = QuestionnaireStatus.Draft;
|
||||||
|
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime? PublishedDate { get; set; } // Nullable - only set when published
|
||||||
|
public DateTime? ArchivedDate { get; set; } // Nullable - only set when archived
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
Model/QuestionnaireStatus.cs
Normal file
15
Model/QuestionnaireStatus.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Model
|
||||||
|
{
|
||||||
|
public enum QuestionnaireStatus
|
||||||
|
{
|
||||||
|
Draft = 0, // Can be fully edited/deleted
|
||||||
|
Published = 1, // Live, accepting responses - limited editing
|
||||||
|
Archived = 2 // Completed, read-only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,72 +13,129 @@ namespace Services.Implemnetation
|
||||||
{
|
{
|
||||||
_context = Context;
|
_context = Context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EXISTING METHOD - Keep exactly as is
|
||||||
public void Add(Questionnaire questionnaire)
|
public void Add(Questionnaire questionnaire)
|
||||||
{
|
{
|
||||||
_context.Questionnaires.Add(questionnaire);
|
_context.Questionnaires.Add(questionnaire);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EXISTING METHOD - Keep exactly as is
|
||||||
public async Task commitAsync()
|
public async Task commitAsync()
|
||||||
{
|
{
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
//public void Delete(int? id)
|
// EXISTING METHOD - Keep exactly as is
|
||||||
//{
|
|
||||||
// var questionnairId = GetQuesById(id);
|
|
||||||
|
|
||||||
// _context.Questionnaires.Remove(questionnairId);
|
|
||||||
//}
|
|
||||||
|
|
||||||
public List<Questionnaire> GetAllQuestions()
|
public List<Questionnaire> GetAllQuestions()
|
||||||
{
|
{
|
||||||
return _context.Questionnaires.ToList();
|
return _context.Questionnaires.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EXISTING METHOD - Keep exactly as is
|
||||||
public Questionnaire GetQuesById(int? id)
|
public Questionnaire GetQuesById(int? id)
|
||||||
{
|
{
|
||||||
return _context.Questionnaires.Find(id);
|
return _context.Questionnaires.Find(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UPDATE THIS METHOD - Add filter for active questions only
|
||||||
public List<Questionnaire> GetQuestionnairesWithQuestion()
|
public List<Questionnaire> GetQuestionnairesWithQuestion()
|
||||||
{
|
{
|
||||||
return _context.Questionnaires.AsNoTracking().Include(x=>x.Questions).ThenInclude(x=>x.Answers).ToList();
|
return _context.Questionnaires
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(x => x.Questions.Where(q => q.IsActive)) // Only get active questions
|
||||||
|
.ThenInclude(x => x.Answers)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UPDATE THIS METHOD - Add filter for active questions only
|
||||||
public Questionnaire GetQuestionnaireWithQuestionAndAnswer(int? id)
|
public Questionnaire GetQuestionnaireWithQuestionAndAnswer(int? id)
|
||||||
{
|
{
|
||||||
return _context.Questionnaires // ✅ No AsNoTracking for edit operations!
|
return _context.Questionnaires
|
||||||
.Include(x => x.Questions)
|
.Include(x => x.Questions.Where(q => q.IsActive)) // Only get active questions
|
||||||
.ThenInclude(x => x.Answers)
|
.ThenInclude(x => x.Answers)
|
||||||
.FirstOrDefault(x => x.Id == id);
|
.FirstOrDefault(x => x.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EXISTING METHOD - Keep exactly as is
|
||||||
public async Task Update(Questionnaire questionnaire)
|
public async Task Update(Questionnaire questionnaire)
|
||||||
{
|
{
|
||||||
|
|
||||||
_context.Questionnaires.Update(questionnaire);
|
_context.Questionnaires.Update(questionnaire);
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EXISTING METHOD - Keep exactly as is
|
||||||
public async Task Delete(int? id)
|
public async Task Delete(int? id)
|
||||||
{
|
{
|
||||||
if (id == null)
|
if (id == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(id), "ID cannot be null");
|
throw new ArgumentNullException(nameof(id), "ID cannot be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
var questionnaire = GetQuesById(id);
|
var questionnaire = GetQuesById(id);
|
||||||
|
|
||||||
if (questionnaire == null)
|
if (questionnaire == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Questionnaire not found", nameof(id));
|
throw new ArgumentException("Questionnaire not found", nameof(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
_context.Questionnaires.Remove(questionnaire);
|
_context.Questionnaires.Remove(questionnaire);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ADD THESE NEW METHODS (for future status management):
|
||||||
|
|
||||||
|
// Get questionnaires by status
|
||||||
|
public List<Questionnaire> GetQuestionnairesByStatus(QuestionnaireStatus status)
|
||||||
|
{
|
||||||
|
return _context.Questionnaires
|
||||||
|
.Where(q => q.Status == status)
|
||||||
|
.Include(x => x.Questions.Where(q => q.IsActive))
|
||||||
|
.ThenInclude(x => x.Answers)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all questionnaires with status info (for admin dashboard)
|
||||||
|
public List<Questionnaire> GetAllQuestionnairesWithStatus()
|
||||||
|
{
|
||||||
|
return _context.Questionnaires
|
||||||
|
.Include(x => x.Questions.Where(q => q.IsActive))
|
||||||
|
.ThenInclude(x => x.Answers)
|
||||||
|
.OrderByDescending(q => q.CreatedDate)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if questionnaire has responses (useful for status changes)
|
||||||
|
public async Task<bool> HasResponses(int questionnaireId)
|
||||||
|
{
|
||||||
|
return await _context.Responses
|
||||||
|
.AnyAsync(r => r.QuestionnaireId == questionnaireId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update questionnaire status
|
||||||
|
public async Task UpdateStatus(int questionnaireId, QuestionnaireStatus newStatus)
|
||||||
|
{
|
||||||
|
var questionnaire = await _context.Questionnaires.FindAsync(questionnaireId);
|
||||||
|
if (questionnaire != null)
|
||||||
|
{
|
||||||
|
questionnaire.Status = newStatus;
|
||||||
|
|
||||||
|
// Set appropriate timestamps
|
||||||
|
switch (newStatus)
|
||||||
|
{
|
||||||
|
case QuestionnaireStatus.Published:
|
||||||
|
if (questionnaire.PublishedDate == null)
|
||||||
|
questionnaire.PublishedDate = DateTime.UtcNow;
|
||||||
|
break;
|
||||||
|
case QuestionnaireStatus.Archived:
|
||||||
|
if (questionnaire.ArchivedDate == null)
|
||||||
|
questionnaire.ArchivedDate = DateTime.UtcNow;
|
||||||
|
break;
|
||||||
|
case QuestionnaireStatus.Draft:
|
||||||
|
questionnaire.PublishedDate = null;
|
||||||
|
questionnaire.ArchivedDate = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Model;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Model;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -19,5 +20,13 @@ namespace Services.Interaces
|
||||||
Task Delete(int? id);
|
Task Delete(int? id);
|
||||||
|
|
||||||
Task commitAsync();
|
Task commitAsync();
|
||||||
|
|
||||||
|
// ADD THESE NEW METHOD SIGNATURES:
|
||||||
|
List<Questionnaire> GetQuestionnairesByStatus(QuestionnaireStatus status);
|
||||||
|
List<Questionnaire> GetAllQuestionnairesWithStatus();
|
||||||
|
Task<bool> HasResponses(int questionnaireId);
|
||||||
|
Task UpdateStatus(int questionnaireId, QuestionnaireStatus newStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,34 +39,63 @@ namespace Web.Areas.Admin.Controllers
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_emailServices = emailServices;
|
_emailServices = emailServices;
|
||||||
}
|
}
|
||||||
public IActionResult Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
|
var questionnaire = _questionnaire.GetAllQuestionnairesWithStatus(); // Use new method
|
||||||
var questionnaire = _questionnaire.GetQuestionnairesWithQuestion();
|
var question = _question.GetQuestionsWithAnswers(); // Keep your existing line
|
||||||
|
|
||||||
var question = _question.GetQuestionsWithAnswers();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
List<QuestionnaireViewModel> viewmodel = new List<QuestionnaireViewModel>();
|
List<QuestionnaireViewModel> viewmodel = new List<QuestionnaireViewModel>();
|
||||||
|
|
||||||
|
|
||||||
foreach (var item in questionnaire)
|
foreach (var item in questionnaire)
|
||||||
{
|
{
|
||||||
|
// Check if this questionnaire has responses
|
||||||
|
var hasResponses = await _questionnaire.HasResponses(item.Id);
|
||||||
|
|
||||||
viewmodel.Add(new QuestionnaireViewModel
|
viewmodel.Add(new QuestionnaireViewModel
|
||||||
{
|
{
|
||||||
|
// EXISTING MAPPING (keep exactly as-is):
|
||||||
Id = item.Id,
|
Id = item.Id,
|
||||||
Description = item.Description,
|
Description = item.Description,
|
||||||
Title = item.Title,
|
Title = item.Title,
|
||||||
Questions = item.Questions,
|
Questions = item.Questions,
|
||||||
|
|
||||||
|
// ADD NEW STATUS MAPPING:
|
||||||
|
Status = item.Status,
|
||||||
|
CreatedDate = item.CreatedDate,
|
||||||
|
PublishedDate = item.PublishedDate,
|
||||||
|
ArchivedDate = item.ArchivedDate,
|
||||||
|
HasResponses = hasResponses
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return View(viewmodel);
|
return View(viewmodel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult StatusGuide()
|
||||||
|
{
|
||||||
|
// Simple documentation page - no model needed
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
// Add this to your controller
|
||||||
|
public async Task<JsonResult> GetQuestionnaireStats(int id)
|
||||||
|
{
|
||||||
|
var responseCount = await _context.Responses
|
||||||
|
.CountAsync(r => r.QuestionnaireId == id);
|
||||||
|
|
||||||
|
var questionCount = await _context.Questions
|
||||||
|
.CountAsync(q => q.QuestionnaireId == id && q.IsActive);
|
||||||
|
|
||||||
|
var questionnaire = await _context.Questionnaires.FindAsync(id);
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
responseCount,
|
||||||
|
questionCount,
|
||||||
|
status = questionnaire?.Status.ToString(),
|
||||||
|
hasResponses = responseCount > 0
|
||||||
|
});
|
||||||
|
}
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult Create()
|
public IActionResult Create()
|
||||||
|
|
||||||
|
|
@ -143,7 +172,7 @@ namespace Web.Areas.Admin.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult Edit(int? id)
|
public IActionResult Edit(int id)
|
||||||
{
|
{
|
||||||
var questionTypes = Enum.GetValues(typeof(QuestionType))
|
var questionTypes = Enum.GetValues(typeof(QuestionType))
|
||||||
.Cast<QuestionType>()
|
.Cast<QuestionType>()
|
||||||
|
|
@ -154,16 +183,17 @@ namespace Web.Areas.Admin.Controllers
|
||||||
|
|
||||||
if (questionnaire == null)
|
if (questionnaire == null)
|
||||||
{
|
{
|
||||||
return NotFound(); // Or handle not found case appropriately
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ADD THIS LINE: Pass questionnaire to view for status checking
|
||||||
|
ViewBag.Questionnaire = questionnaire;
|
||||||
|
|
||||||
var viewModel = new EditQuestionnaireViewModel
|
var viewModel = new EditQuestionnaireViewModel
|
||||||
{
|
{
|
||||||
Id = questionnaire.Id,
|
Id = questionnaire.Id,
|
||||||
Title = questionnaire.Title,
|
Title = questionnaire.Title,
|
||||||
Description = questionnaire.Description,
|
Description = questionnaire.Description,
|
||||||
|
|
||||||
|
|
||||||
Questions = questionnaire.Questions
|
Questions = questionnaire.Questions
|
||||||
.Select(q => new Question
|
.Select(q => new Question
|
||||||
{
|
{
|
||||||
|
|
@ -171,18 +201,13 @@ namespace Web.Areas.Admin.Controllers
|
||||||
Text = q.Text,
|
Text = q.Text,
|
||||||
Type = q.Type,
|
Type = q.Type,
|
||||||
QuestionnaireId = q.QuestionnaireId,
|
QuestionnaireId = q.QuestionnaireId,
|
||||||
|
|
||||||
|
|
||||||
Answers = q.Answers.Select(a => new Answer
|
Answers = q.Answers.Select(a => new Answer
|
||||||
{
|
{
|
||||||
Id = a.Id,
|
Id = a.Id,
|
||||||
Text = a.Text,
|
Text = a.Text,
|
||||||
Question = a.Question,
|
Question = a.Question,
|
||||||
QuestionId = a.QuestionId
|
QuestionId = a.QuestionId,
|
||||||
|
IsOtherOption = a.IsOtherOption
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}).ToList()
|
}).ToList()
|
||||||
}).ToList()
|
}).ToList()
|
||||||
};
|
};
|
||||||
|
|
@ -193,6 +218,10 @@ namespace Web.Areas.Admin.Controllers
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Edit(EditQuestionnaireViewModel viewModel)
|
public async Task<IActionResult> Edit(EditQuestionnaireViewModel viewModel)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine("=== STATUS-AWARE EDIT POST METHOD CALLED ===");
|
||||||
|
Console.WriteLine($"Questionnaire ID: {viewModel?.Id}");
|
||||||
|
Console.WriteLine($"Questions count: {viewModel?.Questions?.Count ?? 0}");
|
||||||
|
|
||||||
var questionTypes = Enum.GetValues(typeof(QuestionType))
|
var questionTypes = Enum.GetValues(typeof(QuestionType))
|
||||||
.Cast<QuestionType>()
|
.Cast<QuestionType>()
|
||||||
.Select(e => new SelectListItem { Value = e.ToString(), Text = e.ToString() });
|
.Select(e => new SelectListItem { Value = e.ToString(), Text = e.ToString() });
|
||||||
|
|
@ -203,10 +232,11 @@ namespace Web.Areas.Admin.Controllers
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||||
|
Console.WriteLine("Database transaction started");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Step 1: Update the questionnaire basic info
|
// Step 1: Get the questionnaire with its current status
|
||||||
var existingQuestionnaire = await _context.Questionnaires
|
var existingQuestionnaire = await _context.Questionnaires
|
||||||
.FirstOrDefaultAsync(q => q.Id == viewModel.Id);
|
.FirstOrDefaultAsync(q => q.Id == viewModel.Id);
|
||||||
|
|
||||||
|
|
@ -215,66 +245,261 @@ namespace Web.Areas.Admin.Controllers
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Questionnaire Status: {existingQuestionnaire.Status}");
|
||||||
|
|
||||||
|
// Step 2: Check if questionnaire can be edited
|
||||||
|
if (existingQuestionnaire.Status == QuestionnaireStatus.Archived)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Archived questionnaires cannot be edited. Please revert to draft status first.";
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Update basic questionnaire info (always allowed)
|
||||||
existingQuestionnaire.Title = viewModel.Title;
|
existingQuestionnaire.Title = viewModel.Title;
|
||||||
existingQuestionnaire.Description = viewModel.Description;
|
existingQuestionnaire.Description = viewModel.Description;
|
||||||
|
|
||||||
// Step 2: Get all existing questions for this questionnaire
|
// Step 4: Get existing questions
|
||||||
var existingQuestions = await _context.Questions
|
var existingQuestions = await _context.Questions
|
||||||
.Where(q => q.QuestionnaireId == viewModel.Id)
|
.Include(q => q.Answers)
|
||||||
|
.Where(q => q.QuestionnaireId == viewModel.Id && q.IsActive)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
// Step 3: Delete ALL answers first (foreign key constraint)
|
Console.WriteLine($"Found {existingQuestions.Count} existing active questions");
|
||||||
if (existingQuestions.Any())
|
|
||||||
|
// Step 5: Handle editing based on questionnaire status
|
||||||
|
switch (existingQuestionnaire.Status)
|
||||||
{
|
{
|
||||||
var questionIds = existingQuestions.Select(q => q.Id).ToList();
|
case QuestionnaireStatus.Draft:
|
||||||
var existingAnswers = await _context.Answers
|
Console.WriteLine("DRAFT MODE: Full editing allowed");
|
||||||
.Where(a => questionIds.Contains(a.QuestionId))
|
await HandleDraftQuestionnaire(viewModel, existingQuestions, existingQuestionnaire.Id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QuestionnaireStatus.Published:
|
||||||
|
Console.WriteLine("PUBLISHED MODE: Limited editing (preserves response data)");
|
||||||
|
await HandlePublishedQuestionnaire(viewModel, existingQuestions, existingQuestionnaire.Id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Final save and commit
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
Console.WriteLine("Transaction committed successfully");
|
||||||
|
|
||||||
|
// Step 7: Success message
|
||||||
|
var finalQuestionCount = await _context.Questions
|
||||||
|
.CountAsync(q => q.QuestionnaireId == viewModel.Id && q.IsActive);
|
||||||
|
|
||||||
|
TempData["Success"] = $"Questionnaire updated successfully with {finalQuestionCount} question(s)!";
|
||||||
|
|
||||||
|
if (existingQuestionnaire.Status == QuestionnaireStatus.Published)
|
||||||
|
{
|
||||||
|
TempData["Info"] = "Limited editing was applied to preserve response data integrity.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"ERROR in transaction: {ex.Message}");
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"ERROR in Edit method: {ex.Message}");
|
||||||
|
ModelState.AddModelError("", $"An error occurred while updating the questionnaire: {ex.Message}");
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("ModelState is NOT valid");
|
||||||
|
foreach (var error in ModelState.Values.SelectMany(v => v.Errors))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Validation error: {error.ErrorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Draft Questionnaires (Full Editing Allowed)
|
||||||
|
private async Task HandleDraftQuestionnaire(EditQuestionnaireViewModel viewModel,
|
||||||
|
List<Question> existingQuestions, int questionnaireId)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Processing DRAFT questionnaire - full editing allowed");
|
||||||
|
|
||||||
|
var incomingQuestionIds = viewModel.Questions?
|
||||||
|
.Where(q => q.Id > 0 && !string.IsNullOrWhiteSpace(q.Text))
|
||||||
|
.Select(q => q.Id)
|
||||||
|
.ToList() ?? new List<int>();
|
||||||
|
|
||||||
|
// HARD DELETE is safe for draft questionnaires (no responses exist)
|
||||||
|
var questionsToDelete = existingQuestions
|
||||||
|
.Where(eq => !incomingQuestionIds.Contains(eq.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (questionsToDelete.Any())
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Hard deleting {questionsToDelete.Count} questions from draft");
|
||||||
|
|
||||||
|
// Delete answers first
|
||||||
|
var questionIdsToDelete = questionsToDelete.Select(q => q.Id).ToList();
|
||||||
|
var answersToDelete = await _context.Answers
|
||||||
|
.Where(a => questionIdsToDelete.Contains(a.QuestionId))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
_context.Answers.RemoveRange(existingAnswers);
|
_context.Answers.RemoveRange(answersToDelete);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Delete questions
|
||||||
|
_context.Questions.RemoveRange(questionsToDelete);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Delete ALL questions
|
// Process remaining questions (full editing allowed)
|
||||||
_context.Questions.RemoveRange(existingQuestions);
|
await ProcessQuestions(viewModel, existingQuestions, questionnaireId, allowFullEditing: true);
|
||||||
await _context.SaveChangesAsync();
|
}
|
||||||
|
|
||||||
// Step 5: Add new questions (only if provided and valid)
|
// Handle Published Questionnaires (Limited Editing)
|
||||||
int newQuestionsAdded = 0;
|
private async Task HandlePublishedQuestionnaire(EditQuestionnaireViewModel viewModel,
|
||||||
if (viewModel.Questions != null && viewModel.Questions.Count > 0)
|
List<Question> existingQuestions, int questionnaireId)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine("Processing PUBLISHED questionnaire - limited editing");
|
||||||
|
|
||||||
|
var incomingQuestionIds = viewModel.Questions?
|
||||||
|
.Where(q => q.Id > 0 && !string.IsNullOrWhiteSpace(q.Text))
|
||||||
|
.Select(q => q.Id)
|
||||||
|
.ToList() ?? new List<int>();
|
||||||
|
|
||||||
|
// SOFT DELETE for published questionnaires (preserve response relationships)
|
||||||
|
var questionsToSoftDelete = existingQuestions
|
||||||
|
.Where(eq => !incomingQuestionIds.Contains(eq.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var question in questionsToSoftDelete)
|
||||||
|
{
|
||||||
|
question.IsActive = false;
|
||||||
|
Console.WriteLine($"Soft deleted question: {question.Text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process remaining questions (limited editing)
|
||||||
|
await ProcessQuestions(viewModel, existingQuestions, questionnaireId, allowFullEditing: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common Question Processing Logic
|
||||||
|
private async Task ProcessQuestions(EditQuestionnaireViewModel viewModel,
|
||||||
|
List<Question> existingQuestions, int questionnaireId, bool allowFullEditing)
|
||||||
|
{
|
||||||
|
if (viewModel.Questions == null) return;
|
||||||
|
|
||||||
var validQuestions = viewModel.Questions
|
var validQuestions = viewModel.Questions
|
||||||
.Where(q => !string.IsNullOrWhiteSpace(q.Text))
|
.Where(q => !string.IsNullOrWhiteSpace(q.Text))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
Console.WriteLine($"Processing {validQuestions.Count} valid questions");
|
||||||
|
|
||||||
foreach (var questionViewModel in validQuestions)
|
foreach (var questionViewModel in validQuestions)
|
||||||
{
|
{
|
||||||
|
if (questionViewModel.Id > 0)
|
||||||
|
{
|
||||||
|
// UPDATE existing question
|
||||||
|
var existingQuestion = existingQuestions.FirstOrDefault(eq => eq.Id == questionViewModel.Id && eq.IsActive);
|
||||||
|
if (existingQuestion != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Updating question {existingQuestion.Id}: '{questionViewModel.Text}'");
|
||||||
|
|
||||||
|
// Always allow text and type updates
|
||||||
|
existingQuestion.Text = questionViewModel.Text.Trim();
|
||||||
|
existingQuestion.Type = questionViewModel.Type;
|
||||||
|
|
||||||
|
// Answer editing depends on questionnaire status and response data
|
||||||
|
if (allowFullEditing)
|
||||||
|
{
|
||||||
|
// Full answer editing (Draft mode)
|
||||||
|
Console.WriteLine($"Full answer editing for question {existingQuestion.Id}");
|
||||||
|
await UpdateQuestionAnswers(existingQuestion, questionViewModel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Limited answer editing (Published mode) - only if no responses
|
||||||
|
var hasResponses = await _context.ResponseDetails
|
||||||
|
.AnyAsync(rd => rd.QuestionId == existingQuestion.Id);
|
||||||
|
|
||||||
|
if (!hasResponses)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"No responses found - updating answers for question {existingQuestion.Id}");
|
||||||
|
await UpdateQuestionAnswers(existingQuestion, questionViewModel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Question {existingQuestion.Id} has responses - preserving answers");
|
||||||
|
TempData["Warning"] = "Some questions have responses, so their answer options were preserved to maintain data integrity.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// CREATE new question (always allowed)
|
||||||
|
Console.WriteLine($"Creating new question: '{questionViewModel.Text}'");
|
||||||
|
|
||||||
var newQuestion = new Question
|
var newQuestion = new Question
|
||||||
{
|
{
|
||||||
Text = questionViewModel.Text.Trim(),
|
Text = questionViewModel.Text.Trim(),
|
||||||
Type = questionViewModel.Type,
|
Type = questionViewModel.Type,
|
||||||
QuestionnaireId = viewModel.Id
|
QuestionnaireId = questionnaireId,
|
||||||
|
IsActive = true,
|
||||||
|
CreatedDate = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
_context.Questions.Add(newQuestion);
|
_context.Questions.Add(newQuestion);
|
||||||
await _context.SaveChangesAsync(); // Save to get the ID
|
await _context.SaveChangesAsync(); // Save to get the ID
|
||||||
|
|
||||||
// Add answers for this question
|
Console.WriteLine($"New question created with ID: {newQuestion.Id}");
|
||||||
if (questionViewModel.Answers != null)
|
|
||||||
|
// Add answers for the new question
|
||||||
|
await AddAnswersToQuestion(newQuestion, questionViewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Update Question Answers
|
||||||
|
private async Task UpdateQuestionAnswers(Question question, Question questionViewModel)
|
||||||
|
{
|
||||||
|
// Remove existing answers
|
||||||
|
var existingAnswers = question.Answers.ToList();
|
||||||
|
if (existingAnswers.Any())
|
||||||
|
{
|
||||||
|
_context.Answers.RemoveRange(existingAnswers);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new answers
|
||||||
|
await AddAnswersToQuestion(question, questionViewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Add Answers to Question
|
||||||
|
private async Task AddAnswersToQuestion(Question question, Question questionViewModel)
|
||||||
|
{
|
||||||
|
if (questionViewModel.Answers != null && questionViewModel.Answers.Any())
|
||||||
{
|
{
|
||||||
var validAnswers = questionViewModel.Answers
|
var validAnswers = questionViewModel.Answers
|
||||||
.Where(a => !string.IsNullOrWhiteSpace(a.Text))
|
.Where(a => !string.IsNullOrWhiteSpace(a.Text))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
Console.WriteLine($"Adding {validAnswers.Count} answers to question {question.Id}");
|
||||||
|
|
||||||
foreach (var answerViewModel in validAnswers)
|
foreach (var answerViewModel in validAnswers)
|
||||||
{
|
{
|
||||||
var newAnswer = new Answer
|
var newAnswer = new Answer
|
||||||
{
|
{
|
||||||
Text = answerViewModel.Text.Trim(),
|
Text = answerViewModel.Text.Trim(),
|
||||||
IsOtherOption = answerViewModel.IsOtherOption,
|
IsOtherOption = answerViewModel.IsOtherOption,
|
||||||
QuestionId = newQuestion.Id
|
QuestionId = question.Id
|
||||||
};
|
};
|
||||||
|
|
||||||
_context.Answers.Add(newAnswer);
|
_context.Answers.Add(newAnswer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,46 +508,6 @@ namespace Web.Areas.Admin.Controllers
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newQuestionsAdded++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 6: Final save and commit
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
await transaction.CommitAsync();
|
|
||||||
|
|
||||||
// Step 7: Get final count for success message
|
|
||||||
var finalQuestionCount = await _context.Questions
|
|
||||||
.Where(q => q.QuestionnaireId == viewModel.Id)
|
|
||||||
.CountAsync();
|
|
||||||
|
|
||||||
// Success message
|
|
||||||
if (finalQuestionCount == 0)
|
|
||||||
{
|
|
||||||
TempData["Success"] = "Questionnaire updated successfully. All questions have been removed.";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TempData["Success"] = $"Questionnaire updated successfully with {finalQuestionCount} question(s).";
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction(nameof(Index));
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
await transaction.RollbackAsync();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("", "An error occurred while updating the questionnaire. Please try again.");
|
|
||||||
return View(viewModel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return View(viewModel);
|
|
||||||
}
|
}
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult Delete(int id)
|
public IActionResult Delete(int id)
|
||||||
|
|
@ -912,6 +1097,161 @@ namespace Web.Areas.Admin.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> PublishQuestionnaire(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var questionnaire = await _context.Questionnaires.FindAsync(id);
|
||||||
|
if (questionnaire == null)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Questionnaire not found.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (questionnaire.Status != QuestionnaireStatus.Draft)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Only draft questionnaires can be published.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if questionnaire has questions
|
||||||
|
var hasQuestions = await _context.Questions
|
||||||
|
.AnyAsync(q => q.QuestionnaireId == id && q.IsActive);
|
||||||
|
|
||||||
|
if (!hasQuestions)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Cannot publish questionnaire without questions.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
await _questionnaire.UpdateStatus(id, QuestionnaireStatus.Published);
|
||||||
|
TempData["Success"] = $"Questionnaire '{questionnaire.Title}' has been published successfully!";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error publishing questionnaire {id}: {ex.Message}");
|
||||||
|
TempData["Error"] = "An error occurred while publishing the questionnaire.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// NEW METHOD 2: Archive Questionnaire
|
||||||
|
// ==================================================
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> ArchiveQuestionnaire(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var questionnaire = await _context.Questionnaires.FindAsync(id);
|
||||||
|
if (questionnaire == null)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Questionnaire not found.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (questionnaire.Status != QuestionnaireStatus.Published)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Only published questionnaires can be archived.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
await _questionnaire.UpdateStatus(id, QuestionnaireStatus.Archived);
|
||||||
|
TempData["Success"] = $"Questionnaire '{questionnaire.Title}' has been archived successfully.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error archiving questionnaire {id}: {ex.Message}");
|
||||||
|
TempData["Error"] = "An error occurred while archiving the questionnaire.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// NEW METHOD 3: Revert to Draft
|
||||||
|
// ==================================================
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RevertToDraft(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var questionnaire = await _context.Questionnaires.FindAsync(id);
|
||||||
|
if (questionnaire == null)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Questionnaire not found.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (questionnaire.Status == QuestionnaireStatus.Draft)
|
||||||
|
{
|
||||||
|
TempData["Warning"] = "Questionnaire is already in draft status.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasResponses = await _questionnaire.HasResponses(id);
|
||||||
|
if (hasResponses)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Cannot revert questionnaire to draft status because it has survey responses.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
await _questionnaire.UpdateStatus(id, QuestionnaireStatus.Draft);
|
||||||
|
TempData["Success"] = $"Questionnaire '{questionnaire.Title}' has been reverted to draft status.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error reverting questionnaire {id}: {ex.Message}");
|
||||||
|
TempData["Error"] = "An error occurred while reverting the questionnaire.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// NEW METHOD 4: Get Status Info (Helper for Views)
|
||||||
|
// ==================================================
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetQuestionnaireStatus(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var questionnaire = await _context.Questionnaires.FindAsync(id);
|
||||||
|
if (questionnaire == null)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = "Questionnaire not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasResponses = await _questionnaire.HasResponses(id);
|
||||||
|
var questionCount = await _context.Questions
|
||||||
|
.CountAsync(q => q.QuestionnaireId == id && q.IsActive);
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
status = questionnaire.Status.ToString(),
|
||||||
|
hasResponses = hasResponses,
|
||||||
|
questionCount = questionCount,
|
||||||
|
createdDate = questionnaire.CreatedDate,
|
||||||
|
publishedDate = questionnaire.PublishedDate,
|
||||||
|
archivedDate = questionnaire.ArchivedDate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error getting status for questionnaire {id}: {ex.Message}");
|
||||||
|
return Json(new { success = false, message = "Error retrieving status" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Request models - Add these classes to your project
|
// Request models - Add these classes to your project
|
||||||
public class SaveAnswerConditionRequest
|
public class SaveAnswerConditionRequest
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -14,59 +14,84 @@ namespace Web.Areas.Admin.Controllers
|
||||||
{
|
{
|
||||||
private readonly SurveyContext _context;
|
private readonly SurveyContext _context;
|
||||||
private readonly IUserResponseRepository _userResponse;
|
private readonly IUserResponseRepository _userResponse;
|
||||||
|
private readonly ILogger<UserResponseController> _logger;
|
||||||
|
|
||||||
public UserResponseController(SurveyContext context, IUserResponseRepository userResponse)
|
public UserResponseController(
|
||||||
|
SurveyContext context,
|
||||||
|
IUserResponseRepository userResponse,
|
||||||
|
ILogger<UserResponseController> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_userResponse = userResponse;
|
_userResponse = userResponse;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var responses = await GetAllResponsesWithDetailsAsync(); // Fetch the data
|
try
|
||||||
return View(responses); // Pass the data to the view
|
{
|
||||||
|
var responses = await GetAllResponsesWithDetailsAsync();
|
||||||
|
return View(responses);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error retrieving responses");
|
||||||
|
TempData["Error"] = "Error loading responses. Please try again.";
|
||||||
|
return View(new List<Response>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<Response>> GetAllResponsesWithDetailsAsync()
|
private async Task<List<Response>> GetAllResponsesWithDetailsAsync()
|
||||||
{
|
{
|
||||||
return await _context.Responses
|
return await _context.Responses
|
||||||
.Include(r => r.Questionnaire) // Ensure the Questionnaire data is included
|
.Include(r => r.Questionnaire)
|
||||||
.OrderBy(r => r.Id) // Optional: Order by submission date
|
.OrderByDescending(r => r.SubmissionDate) // Most recent first
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> ViewResponse(int id) // Pass the response ID
|
public async Task<IActionResult> ViewResponse(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var response = await _context.Responses
|
var response = await _context.Responses
|
||||||
.Include(r => r.ResponseDetails)
|
.Include(r => r.ResponseDetails)
|
||||||
.ThenInclude(rd => rd.Question)
|
.ThenInclude(rd => rd.Question)
|
||||||
.ThenInclude(q => q.Answers) // Load all possible answers for the questions
|
.ThenInclude(q => q.Answers)
|
||||||
.Include(r => r.ResponseDetails)
|
.Include(r => r.ResponseDetails)
|
||||||
.ThenInclude(rd => rd.ResponseAnswers) // Load the answers selected by the user
|
.ThenInclude(rd => rd.ResponseAnswers)
|
||||||
|
.Include(r => r.Questionnaire)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(r => r.Id == id);
|
.FirstOrDefaultAsync(r => r.Id == id);
|
||||||
|
|
||||||
if (response == null)
|
if (response == null)
|
||||||
{
|
{
|
||||||
return NotFound(); // If no response is found, return a NotFound result
|
TempData["Error"] = "Response not found.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
return View(response); // Pass the response to the view
|
return View(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error retrieving response {ResponseId}", id);
|
||||||
|
TempData["Error"] = "Error loading response details.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<IActionResult> UserResponsesStatus(string userName)
|
public async Task<IActionResult> UserResponsesStatus(string userName)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var responses = await _userResponse.GetResponsesByUserAsync(userName);
|
var responses = await _userResponse.GetResponsesByUserAsync(userName);
|
||||||
|
|
||||||
if (responses == null || !responses.Any())
|
if (responses == null || !responses.Any())
|
||||||
{
|
{
|
||||||
return NotFound();
|
TempData["Warning"] = "No responses found for this user.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
var userEmail = responses.First().UserEmail;
|
var userEmail = responses.First().UserEmail;
|
||||||
|
|
||||||
var viewModel = new UserResponsesViewModel
|
var viewModel = new UserResponsesViewModel
|
||||||
{
|
{
|
||||||
UserName = userName,
|
UserName = userName,
|
||||||
|
|
@ -76,39 +101,178 @@ namespace Web.Areas.Admin.Controllers
|
||||||
|
|
||||||
return View(viewModel);
|
return View(viewModel);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error retrieving user responses for {UserName}", userName);
|
||||||
|
TempData["Error"] = "Error loading user responses.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> Delete(int id)
|
public async Task<IActionResult> Delete(int id)
|
||||||
{
|
{
|
||||||
var response = await _context.Responses.FindAsync(id);
|
try
|
||||||
|
{
|
||||||
|
var response = await _context.Responses
|
||||||
|
.Include(r => r.ResponseDetails)
|
||||||
|
.ThenInclude(rd => rd.ResponseAnswers)
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == id);
|
||||||
|
|
||||||
if (response == null)
|
if (response == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
TempData["Error"] = "Response not found.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove related data first
|
||||||
|
if (response.ResponseDetails != null)
|
||||||
|
{
|
||||||
|
foreach (var detail in response.ResponseDetails)
|
||||||
|
{
|
||||||
|
if (detail.ResponseAnswers != null)
|
||||||
|
{
|
||||||
|
_context.ResponseAnswers.RemoveRange(detail.ResponseAnswers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_context.ResponseDetails.RemoveRange(response.ResponseDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
_context.Responses.Remove(response);
|
_context.Responses.Remove(response);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return RedirectToAction(nameof(Index));
|
|
||||||
|
_logger.LogInformation("Response {ResponseId} deleted successfully", id);
|
||||||
|
TempData["Success"] = "Response deleted successfully.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting response {ResponseId}", id);
|
||||||
|
TempData["Error"] = "Error deleting response. Please try again.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> DeleteMultiple(int[] ids)
|
public async Task<IActionResult> DeleteMultiple(List<int> ids)
|
||||||
{
|
{
|
||||||
var responses = _context.Responses.Where(r => ids.Contains(r.Id));
|
if (ids == null || !ids.Any())
|
||||||
|
{
|
||||||
_context.Responses.RemoveRange(responses);
|
TempData["Warning"] = "No responses selected for deletion.";
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
TempData["Success"] = "User response deleted successfully";
|
|
||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Attempting to delete {Count} responses: {Ids}", ids.Count, string.Join(", ", ids));
|
||||||
|
|
||||||
|
var responses = await _context.Responses
|
||||||
|
.Include(r => r.ResponseDetails)
|
||||||
|
.ThenInclude(rd => rd.ResponseAnswers)
|
||||||
|
.Where(r => ids.Contains(r.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!responses.Any())
|
||||||
|
{
|
||||||
|
TempData["Warning"] = "No responses found to delete.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove related data first
|
||||||
|
foreach (var response in responses)
|
||||||
|
{
|
||||||
|
if (response.ResponseDetails != null)
|
||||||
|
{
|
||||||
|
foreach (var detail in response.ResponseDetails)
|
||||||
|
{
|
||||||
|
if (detail.ResponseAnswers != null)
|
||||||
|
{
|
||||||
|
_context.ResponseAnswers.RemoveRange(detail.ResponseAnswers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_context.ResponseDetails.RemoveRange(response.ResponseDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.Responses.RemoveRange(responses);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully deleted {Count} responses", responses.Count);
|
||||||
|
TempData["Success"] = $"Successfully deleted {responses.Count} response{(responses.Count > 1 ? "s" : "")}.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting multiple responses. IDs: {Ids}", string.Join(", ", ids));
|
||||||
|
TempData["Error"] = "Error deleting responses. Please try again.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> DeleteAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var allResponses = await _context.Responses
|
||||||
|
.Include(r => r.ResponseDetails)
|
||||||
|
.ThenInclude(rd => rd.ResponseAnswers)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!allResponses.Any())
|
||||||
|
{
|
||||||
|
TempData["Warning"] = "No responses to delete.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all related data
|
||||||
|
foreach (var response in allResponses)
|
||||||
|
{
|
||||||
|
if (response.ResponseDetails != null)
|
||||||
|
{
|
||||||
|
foreach (var detail in response.ResponseDetails)
|
||||||
|
{
|
||||||
|
if (detail.ResponseAnswers != null)
|
||||||
|
{
|
||||||
|
_context.ResponseAnswers.RemoveRange(detail.ResponseAnswers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_context.ResponseDetails.RemoveRange(response.ResponseDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.Responses.RemoveRange(allResponses);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully deleted all {Count} responses", allResponses.Count);
|
||||||
|
TempData["Success"] = $"Successfully deleted all {allResponses.Count} responses.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting all responses");
|
||||||
|
TempData["Error"] = "Error deleting all responses. Please try again.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// API endpoint to check if responses exist
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> CheckResponseExists(int id)
|
||||||
|
{
|
||||||
|
var exists = await _context.Responses.AnyAsync(r => r.Id == id);
|
||||||
|
return Json(new { exists });
|
||||||
|
}
|
||||||
|
|
||||||
|
// API endpoint to get response count
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetResponseCount()
|
||||||
|
{
|
||||||
|
var count = await _context.Responses.CountAsync();
|
||||||
|
return Json(new { count });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,20 +14,28 @@ namespace Web.Areas.Admin.Controllers
|
||||||
{
|
{
|
||||||
private readonly SurveyContext _context;
|
private readonly SurveyContext _context;
|
||||||
private readonly IUserResponseRepository _userResponse;
|
private readonly IUserResponseRepository _userResponse;
|
||||||
|
private readonly ILogger<UserResponseStatusController> _logger;
|
||||||
|
|
||||||
public UserResponseStatusController(SurveyContext context, IUserResponseRepository userResponse)
|
public UserResponseStatusController(
|
||||||
|
SurveyContext context,
|
||||||
|
IUserResponseRepository userResponse,
|
||||||
|
ILogger<UserResponseStatusController> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_userResponse = userResponse;
|
_userResponse = userResponse;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var usersWithQuestionnaires = await _context.Responses
|
var usersWithQuestionnaires = await _context.Responses
|
||||||
.Include(r => r.Questionnaire)
|
.Include(r => r.Questionnaire)
|
||||||
.GroupBy(r => r.UserEmail)
|
.GroupBy(r => r.UserEmail)
|
||||||
.Select(g => new UserResponsesViewModel
|
.Select(g => new UserResponsesViewModel
|
||||||
{
|
{
|
||||||
UserName = g.FirstOrDefault().UserName, // Display the first username found for the email
|
UserName = g.FirstOrDefault().UserName,
|
||||||
UserEmail = g.Key,
|
UserEmail = g.Key,
|
||||||
Responses = g.Select(r => new Response
|
Responses = g.Select(r => new Response
|
||||||
{
|
{
|
||||||
|
|
@ -38,8 +46,17 @@ namespace Web.Areas.Admin.Controllers
|
||||||
|
|
||||||
return View(usersWithQuestionnaires);
|
return View(usersWithQuestionnaires);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error retrieving user responses");
|
||||||
|
TempData["Error"] = "Error loading user responses. Please try again.";
|
||||||
|
return View(new List<UserResponsesViewModel>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> UserResponsesStatus(string userEmail)
|
public async Task<IActionResult> UserResponsesStatus(string userEmail)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var responses = await _context.Responses
|
var responses = await _context.Responses
|
||||||
.Include(r => r.Questionnaire)
|
.Include(r => r.Questionnaire)
|
||||||
|
|
@ -54,7 +71,8 @@ namespace Web.Areas.Admin.Controllers
|
||||||
|
|
||||||
if (responses == null || !responses.Any())
|
if (responses == null || !responses.Any())
|
||||||
{
|
{
|
||||||
return NotFound();
|
TempData["Warning"] = "No responses found for this user.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
var userName = responses.First().UserName;
|
var userName = responses.First().UserName;
|
||||||
|
|
@ -68,29 +86,134 @@ namespace Web.Areas.Admin.Controllers
|
||||||
|
|
||||||
return View(viewModel);
|
return View(viewModel);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error retrieving user responses for {UserEmail}", userEmail);
|
||||||
|
TempData["Error"] = "Error loading user response details.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> DeleteSelected(string[] selectedEmails)
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> DeleteSelected(List<string> selectedEmails)
|
||||||
{
|
{
|
||||||
if (selectedEmails == null || selectedEmails.Length == 0)
|
if (selectedEmails == null || !selectedEmails.Any())
|
||||||
{
|
{
|
||||||
|
TempData["Warning"] = "No users selected for deletion.";
|
||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Attempting to delete responses for {Count} users: {Emails}",
|
||||||
|
selectedEmails.Count, string.Join(", ", selectedEmails));
|
||||||
|
|
||||||
var responsesToDelete = await _context.Responses
|
var responsesToDelete = await _context.Responses
|
||||||
|
.Include(r => r.ResponseDetails)
|
||||||
|
.ThenInclude(rd => rd.ResponseAnswers)
|
||||||
.Where(r => selectedEmails.Contains(r.UserEmail))
|
.Where(r => selectedEmails.Contains(r.UserEmail))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
if (responsesToDelete.Any())
|
if (!responsesToDelete.Any())
|
||||||
{
|
{
|
||||||
|
TempData["Warning"] = "No responses found for the selected users.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove related data first to avoid foreign key constraints
|
||||||
|
foreach (var response in responsesToDelete)
|
||||||
|
{
|
||||||
|
if (response.ResponseDetails != null)
|
||||||
|
{
|
||||||
|
foreach (var detail in response.ResponseDetails)
|
||||||
|
{
|
||||||
|
if (detail.ResponseAnswers != null)
|
||||||
|
{
|
||||||
|
_context.ResponseAnswers.RemoveRange(detail.ResponseAnswers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_context.ResponseDetails.RemoveRange(response.ResponseDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_context.Responses.RemoveRange(responsesToDelete);
|
_context.Responses.RemoveRange(responsesToDelete);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully deleted {Count} responses for {UserCount} users",
|
||||||
|
responsesToDelete.Count, selectedEmails.Count);
|
||||||
|
|
||||||
|
TempData["Success"] = $"Successfully deleted responses for {selectedEmails.Count} user{(selectedEmails.Count > 1 ? "s" : "")} ({responsesToDelete.Count} response{(responsesToDelete.Count > 1 ? "s" : "")} total).";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting responses for users: {Emails}", string.Join(", ", selectedEmails));
|
||||||
|
TempData["Error"] = "Error deleting user responses. Please try again.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> DeleteUserResponses(string userEmail)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(userEmail))
|
||||||
|
{
|
||||||
|
TempData["Error"] = "User email is required.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var responsesToDelete = await _context.Responses
|
||||||
|
.Include(r => r.ResponseDetails)
|
||||||
|
.ThenInclude(rd => rd.ResponseAnswers)
|
||||||
|
.Where(r => r.UserEmail == userEmail)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!responsesToDelete.Any())
|
||||||
|
{
|
||||||
|
TempData["Warning"] = "No responses found for this user.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove related data first
|
||||||
|
foreach (var response in responsesToDelete)
|
||||||
|
{
|
||||||
|
if (response.ResponseDetails != null)
|
||||||
|
{
|
||||||
|
foreach (var detail in response.ResponseDetails)
|
||||||
|
{
|
||||||
|
if (detail.ResponseAnswers != null)
|
||||||
|
{
|
||||||
|
_context.ResponseAnswers.RemoveRange(detail.ResponseAnswers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_context.ResponseDetails.RemoveRange(response.ResponseDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.Responses.RemoveRange(responsesToDelete);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully deleted {Count} responses for user {UserEmail}",
|
||||||
|
responsesToDelete.Count, userEmail);
|
||||||
|
|
||||||
|
TempData["Success"] = $"Successfully deleted all responses for user {userEmail}.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting responses for user {UserEmail}", userEmail);
|
||||||
|
TempData["Error"] = "Error deleting user responses. Please try again.";
|
||||||
}
|
}
|
||||||
|
|
||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> GenerateReport(string userEmail, string format)
|
public async Task<IActionResult> GenerateReport(string userEmail, string format)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var responses = await _context.Responses
|
var responses = await _context.Responses
|
||||||
.Include(r => r.Questionnaire)
|
.Include(r => r.Questionnaire)
|
||||||
|
|
@ -104,7 +227,8 @@ namespace Web.Areas.Admin.Controllers
|
||||||
|
|
||||||
if (responses == null || !responses.Any())
|
if (responses == null || !responses.Any())
|
||||||
{
|
{
|
||||||
return NotFound();
|
TempData["Warning"] = "No responses found for this user.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (format.ToLower())
|
switch (format.ToLower())
|
||||||
|
|
@ -114,7 +238,15 @@ namespace Web.Areas.Admin.Controllers
|
||||||
case "excel":
|
case "excel":
|
||||||
return GenerateExcelReport(responses);
|
return GenerateExcelReport(responses);
|
||||||
default:
|
default:
|
||||||
return BadRequest("Unsupported report format.");
|
TempData["Error"] = "Unsupported report format.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error generating report for user {UserEmail}", userEmail);
|
||||||
|
TempData["Error"] = "Error generating report. Please try again.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,7 +258,7 @@ namespace Web.Areas.Admin.Controllers
|
||||||
var stream = new MemoryStream();
|
var stream = new MemoryStream();
|
||||||
var document = new Document(PageSize.A4, 50, 50, 25, 25);
|
var document = new Document(PageSize.A4, 50, 50, 25, 25);
|
||||||
var writer = PdfWriter.GetInstance(document, stream);
|
var writer = PdfWriter.GetInstance(document, stream);
|
||||||
writer.CloseStream = false; // Prevent the stream from being closed when the document is closed
|
writer.CloseStream = false;
|
||||||
|
|
||||||
document.Open();
|
document.Open();
|
||||||
|
|
||||||
|
|
@ -190,7 +322,7 @@ namespace Web.Areas.Admin.Controllers
|
||||||
table.AddCell(new PdfPCell(new Phrase("Answers:", cellFont)) { Padding = 5 });
|
table.AddCell(new PdfPCell(new Phrase("Answers:", cellFont)) { Padding = 5 });
|
||||||
var answers = string.Join(", ", detail.ResponseAnswers.Select(a => detail.Question.Answers.FirstOrDefault(ans => ans.Id == a.AnswerId)?.Text));
|
var answers = string.Join(", ", detail.ResponseAnswers.Select(a => detail.Question.Answers.FirstOrDefault(ans => ans.Id == a.AnswerId)?.Text));
|
||||||
|
|
||||||
// NEW: Include "Other" text if available
|
// Include "Other" text if available
|
||||||
if (!string.IsNullOrEmpty(detail.OtherText))
|
if (!string.IsNullOrEmpty(detail.OtherText))
|
||||||
{
|
{
|
||||||
answers += string.IsNullOrEmpty(answers)
|
answers += string.IsNullOrEmpty(answers)
|
||||||
|
|
@ -228,7 +360,7 @@ namespace Web.Areas.Admin.Controllers
|
||||||
var logo = new FileInfo(logoPath);
|
var logo = new FileInfo(logoPath);
|
||||||
var picture = worksheet.Drawings.AddPicture("Logo", logo);
|
var picture = worksheet.Drawings.AddPicture("Logo", logo);
|
||||||
picture.SetPosition(0, 0, 0, 0);
|
picture.SetPosition(0, 0, 0, 0);
|
||||||
picture.SetSize(300, 70); // Adjust the size as needed
|
picture.SetSize(300, 70);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a title
|
// Add a title
|
||||||
|
|
@ -272,7 +404,7 @@ namespace Web.Areas.Admin.Controllers
|
||||||
{
|
{
|
||||||
var answers = string.Join(", ", detail.ResponseAnswers.Select(a => detail.Question.Answers.FirstOrDefault(ans => ans.Id == a.AnswerId)?.Text));
|
var answers = string.Join(", ", detail.ResponseAnswers.Select(a => detail.Question.Answers.FirstOrDefault(ans => ans.Id == a.AnswerId)?.Text));
|
||||||
|
|
||||||
// NEW: Include "Other" text if available
|
// Include "Other" text if available
|
||||||
if (!string.IsNullOrEmpty(detail.OtherText))
|
if (!string.IsNullOrEmpty(detail.OtherText))
|
||||||
{
|
{
|
||||||
answers += string.IsNullOrEmpty(answers)
|
answers += string.IsNullOrEmpty(answers)
|
||||||
|
|
@ -298,6 +430,8 @@ namespace Web.Areas.Admin.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> GenerateQuestionnairePdfReport(int questionnaireId)
|
public async Task<IActionResult> GenerateQuestionnairePdfReport(int questionnaireId)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var response = await _context.Responses
|
var response = await _context.Responses
|
||||||
.Include(r => r.Questionnaire)
|
.Include(r => r.Questionnaire)
|
||||||
|
|
@ -310,11 +444,19 @@ namespace Web.Areas.Admin.Controllers
|
||||||
|
|
||||||
if (response == null)
|
if (response == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
TempData["Warning"] = "No response found for this questionnaire.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
return GeneratePdfReportForQuestionnaire(response);
|
return GeneratePdfReportForQuestionnaire(response);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error generating questionnaire PDF report for {QuestionnaireId}", questionnaireId);
|
||||||
|
TempData["Error"] = "Error generating PDF report. Please try again.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private IActionResult GeneratePdfReportForQuestionnaire(Response response)
|
private IActionResult GeneratePdfReportForQuestionnaire(Response response)
|
||||||
{
|
{
|
||||||
|
|
@ -324,7 +466,7 @@ namespace Web.Areas.Admin.Controllers
|
||||||
var stream = new MemoryStream();
|
var stream = new MemoryStream();
|
||||||
var document = new Document(PageSize.A4, 50, 50, 25, 25);
|
var document = new Document(PageSize.A4, 50, 50, 25, 25);
|
||||||
var writer = PdfWriter.GetInstance(document, stream);
|
var writer = PdfWriter.GetInstance(document, stream);
|
||||||
writer.CloseStream = false; // Prevent the stream from being closed when the document is closed
|
writer.CloseStream = false;
|
||||||
|
|
||||||
document.Open();
|
document.Open();
|
||||||
|
|
||||||
|
|
@ -394,7 +536,7 @@ namespace Web.Areas.Admin.Controllers
|
||||||
table.AddCell(new PdfPCell(new Phrase("Answers:", cellFont)) { Padding = 5 });
|
table.AddCell(new PdfPCell(new Phrase("Answers:", cellFont)) { Padding = 5 });
|
||||||
var answers = string.Join(", ", detail.ResponseAnswers.Select(a => detail.Question.Answers.FirstOrDefault(ans => ans.Id == a.AnswerId)?.Text));
|
var answers = string.Join(", ", detail.ResponseAnswers.Select(a => detail.Question.Answers.FirstOrDefault(ans => ans.Id == a.AnswerId)?.Text));
|
||||||
|
|
||||||
// NEW: Include "Other" text if available
|
// Include "Other" text if available
|
||||||
if (!string.IsNullOrEmpty(detail.OtherText))
|
if (!string.IsNullOrEmpty(detail.OtherText))
|
||||||
{
|
{
|
||||||
answers += string.IsNullOrEmpty(answers)
|
answers += string.IsNullOrEmpty(answers)
|
||||||
|
|
@ -415,6 +557,8 @@ namespace Web.Areas.Admin.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> GenerateQuestionnaireExcelReport(int questionnaireId)
|
public async Task<IActionResult> GenerateQuestionnaireExcelReport(int questionnaireId)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var response = await _context.Responses
|
var response = await _context.Responses
|
||||||
.Include(r => r.Questionnaire)
|
.Include(r => r.Questionnaire)
|
||||||
|
|
@ -427,11 +571,19 @@ namespace Web.Areas.Admin.Controllers
|
||||||
|
|
||||||
if (response == null)
|
if (response == null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
TempData["Warning"] = "No response found for this questionnaire.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
return GenerateExcelReportForQuestionnaire(response);
|
return GenerateExcelReportForQuestionnaire(response);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error generating questionnaire Excel report for {QuestionnaireId}", questionnaireId);
|
||||||
|
TempData["Error"] = "Error generating Excel report. Please try again.";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private IActionResult GenerateExcelReportForQuestionnaire(Response response)
|
private IActionResult GenerateExcelReportForQuestionnaire(Response response)
|
||||||
{
|
{
|
||||||
|
|
@ -449,7 +601,7 @@ namespace Web.Areas.Admin.Controllers
|
||||||
var logo = new FileInfo(logoPath);
|
var logo = new FileInfo(logoPath);
|
||||||
var picture = worksheet.Drawings.AddPicture("Logo", logo);
|
var picture = worksheet.Drawings.AddPicture("Logo", logo);
|
||||||
picture.SetPosition(0, 0, 2, 0);
|
picture.SetPosition(0, 0, 2, 0);
|
||||||
picture.SetSize(300, 60); // Adjust the size as needed
|
picture.SetSize(300, 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user details
|
// Add user details
|
||||||
|
|
@ -498,7 +650,7 @@ namespace Web.Areas.Admin.Controllers
|
||||||
{
|
{
|
||||||
var answers = string.Join(", ", detail.ResponseAnswers.Select(a => detail.Question.Answers.FirstOrDefault(ans => ans.Id == a.AnswerId)?.Text));
|
var answers = string.Join(", ", detail.ResponseAnswers.Select(a => detail.Question.Answers.FirstOrDefault(ans => ans.Id == a.AnswerId)?.Text));
|
||||||
|
|
||||||
// NEW: Include "Other" text if available
|
// Include "Other" text if available
|
||||||
if (!string.IsNullOrEmpty(detail.OtherText))
|
if (!string.IsNullOrEmpty(detail.OtherText))
|
||||||
{
|
{
|
||||||
answers += string.IsNullOrEmpty(answers)
|
answers += string.IsNullOrEmpty(answers)
|
||||||
|
|
@ -520,5 +672,21 @@ namespace Web.Areas.Admin.Controllers
|
||||||
return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $"{response.Questionnaire.Title}_{userEmail}.xlsx");
|
return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $"{response.Questionnaire.Title}_{userEmail}.xlsx");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API endpoint to check if user responses exist
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> CheckUserResponsesExist(string userEmail)
|
||||||
|
{
|
||||||
|
var exists = await _context.Responses.AnyAsync(r => r.UserEmail == userEmail);
|
||||||
|
return Json(new { exists });
|
||||||
|
}
|
||||||
|
|
||||||
|
// API endpoint to get user response count
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetUserResponseCount()
|
||||||
|
{
|
||||||
|
var count = await _context.Responses.GroupBy(r => r.UserEmail).CountAsync();
|
||||||
|
return Json(new { count });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,48 +1,905 @@
|
||||||
@model SendQuestionnaireViewModel
|
@model SendQuestionnaireViewModel
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Send";
|
ViewData["Title"] = "Send Questionnaire";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Modern Design System with Advanced Visual Effects */
|
||||||
|
@@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #6366f1;
|
||||||
|
--primary-light: #8b5cf6;
|
||||||
|
--primary-dark: #4338ca;
|
||||||
|
--primary-ultra: #3730a3;
|
||||||
|
--success-color: #10b981;
|
||||||
|
--success-light: #34d399;
|
||||||
|
--success-dark: #059669;
|
||||||
|
--info-color: #06b6d4;
|
||||||
|
--info-light: #22d3ee;
|
||||||
|
--info-dark: #0891b2;
|
||||||
|
--warning-color: #f59e0b;
|
||||||
|
--warning-light: #fbbf24;
|
||||||
|
--warning-dark: #d97706;
|
||||||
|
--danger-color: #ef4444;
|
||||||
|
--danger-light: #fca5a5;
|
||||||
|
--danger-dark: #dc2626;
|
||||||
|
--gray-50: #f8fafc;
|
||||||
|
--gray-100: #f1f5f9;
|
||||||
|
--gray-200: #e2e8f0;
|
||||||
|
--gray-300: #cbd5e1;
|
||||||
|
--gray-400: #94a3b8;
|
||||||
|
--gray-500: #64748b;
|
||||||
|
--gray-600: #475569;
|
||||||
|
--gray-700: #334155;
|
||||||
|
--gray-800: #1e293b;
|
||||||
|
--gray-900: #0f172a;
|
||||||
|
/* Advanced Shadows */
|
||||||
|
--shadow-glow: 0 0 40px rgba(99, 102, 241, 0.15);
|
||||||
|
--shadow-ultra: 0 32px 64px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
--shadow-floating: 0 20px 40px -8px rgba(99, 102, 241, 0.2);
|
||||||
|
--shadow-premium: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 80px rgba(99, 102, 241, 0.1);
|
||||||
|
/* Border Radius */
|
||||||
|
--border-radius-sm: 12px;
|
||||||
|
--border-radius-md: 16px;
|
||||||
|
--border-radius-lg: 24px;
|
||||||
|
--border-radius-xl: 32px;
|
||||||
|
--border-radius-2xl: 40px;
|
||||||
|
/* Animations */
|
||||||
|
--ease-premium: cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
<div class="card justify-content-center p-4 shadow rounded">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title h5">Send the questionnaire</h5>
|
|
||||||
|
|
||||||
<div class="row">
|
/* Premium Glass Morphism Header */
|
||||||
|
.page-header {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: var(--border-radius-2xl);
|
||||||
|
box-shadow: var(--shadow-premium);
|
||||||
|
padding: 3rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: slideDown 0.8s var(--ease-premium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 6px;
|
||||||
|
background: linear-gradient(90deg, var(--primary-color), var(--info-color), var(--success-color), var(--warning-color), var(--primary-light) );
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: gradientFlow 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@keyframes gradientFlow {
|
||||||
|
0%, 100%
|
||||||
|
|
||||||
|
{
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
|
||||||
|
animation: shimmer 4s ease-in-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@keyframes shimmer {
|
||||||
|
0%, 100%
|
||||||
|
|
||||||
|
{
|
||||||
|
transform: rotate(0deg) scale(0.8);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: rotate(180deg) scale(1.2);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 50%, var(--info-color) 100%);
|
||||||
|
color: white;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2.25rem;
|
||||||
|
box-shadow: var(--shadow-floating);
|
||||||
|
position: relative;
|
||||||
|
animation: iconPulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
background: linear-gradient(45deg, var(--primary-color), var(--info-color), var(--success-color), var(--primary-color));
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
z-index: -1;
|
||||||
|
background-size: 300% 300%;
|
||||||
|
animation: borderFlow 3s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@keyframes iconPulse {
|
||||||
|
0%, 100%
|
||||||
|
|
||||||
|
{
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@@keyframes borderFlow {
|
||||||
|
0%, 100%
|
||||||
|
|
||||||
|
{
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 900;
|
||||||
|
background: linear-gradient(135deg, var(--gray-800) 0%, var(--primary-color) 50%, var(--info-color) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
animation: textShimmer 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@keyframes textShimmer {
|
||||||
|
0%, 100%
|
||||||
|
|
||||||
|
{
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-subtitle {
|
||||||
|
color: var(--gray-700);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.6;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Info Cards */
|
||||||
|
.info-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: var(--border-radius-xl);
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: var(--shadow-floating);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.4s var(--ease-premium);
|
||||||
|
animation: slideUp 0.8s var(--ease-premium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card:nth-child(1) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card:nth-child(2) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, var(--info-color), var(--success-color));
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card:hover {
|
||||||
|
transform: translateY(-8px) scale(1.02);
|
||||||
|
box-shadow: var(--shadow-ultra);
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card:hover::before {
|
||||||
|
height: 6px;
|
||||||
|
background: linear-gradient(90deg, var(--primary-color), var(--info-color), var(--success-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card h4 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--gray-800);
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-icon {
|
||||||
|
background: linear-gradient(135deg, var(--info-color), var(--info-light));
|
||||||
|
color: white;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ultra-Premium Form Card */
|
||||||
|
.form-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(30px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
border-radius: var(--border-radius-2xl);
|
||||||
|
box-shadow: var(--shadow-premium);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
animation: slideUp 0.8s var(--ease-premium) 0.6s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 6px;
|
||||||
|
background: linear-gradient(90deg, var(--success-color), var(--info-color), var(--primary-color), var(--warning-color), var(--success-color) );
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: gradientFlow 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(139, 92, 246, 0.05) 50%, rgba(6, 182, 212, 0.05) 100% );
|
||||||
|
padding: 3rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-icon {
|
||||||
|
background: linear-gradient(135deg, var(--success-color) 0%, var(--success-light) 100%);
|
||||||
|
color: white;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
box-shadow: var(--shadow-floating);
|
||||||
|
position: relative;
|
||||||
|
animation: iconBounce 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@keyframes iconBounce {
|
||||||
|
0%, 100%
|
||||||
|
|
||||||
|
{
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
background: linear-gradient(135deg, var(--gray-800), var(--primary-color));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body {
|
||||||
|
padding: 3rem;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ultra-Premium Form Groups */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Beautiful Labels */
|
||||||
|
.control-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--gray-800);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label i {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
|
||||||
|
color: white;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -6px;
|
||||||
|
left: 2.5rem;
|
||||||
|
width: 60px;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, var(--primary-color), var(--info-color));
|
||||||
|
border-radius: 2px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Form Controls */
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border: 2px solid rgba(203, 213, 225, 0.6);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--gray-800);
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
transition: all 0.3s var(--ease-premium);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15), 0 8px 25px rgba(99, 102, 241, 0.2), inset 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
transform: translateY(-2px) scale(1.01);
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:disabled {
|
||||||
|
background: linear-gradient(135deg, var(--info-color), var(--info-light));
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: var(--shadow-floating);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:disabled:focus {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: var(--shadow-floating);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Special Textarea */
|
||||||
|
textarea.form-control {
|
||||||
|
min-height: 140px;
|
||||||
|
resize: vertical;
|
||||||
|
line-height: 1.7;
|
||||||
|
font-family: 'JetBrains Mono', 'Monaco', monospace;
|
||||||
|
background: linear-gradient(135deg, rgba(248, 250, 252, 0.9), rgba(241, 245, 249, 0.9));
|
||||||
|
border-color: var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.form-control:focus {
|
||||||
|
border-color: var(--info-dark);
|
||||||
|
box-shadow: 0 0 0 4px rgba(6, 182, 212, 0.15), 0 8px 25px rgba(6, 182, 212, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DateTime Input */
|
||||||
|
.datetime-input {
|
||||||
|
background: linear-gradient(135deg, rgba(245, 158, 11, 0.05), rgba(251, 191, 36, 0.05));
|
||||||
|
border-color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.datetime-input:focus {
|
||||||
|
border-color: var(--warning-dark);
|
||||||
|
box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.15), 0 8px 25px rgba(245, 158, 11, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ultra-Premium Submit Button */
|
||||||
|
.btn-submit {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 50%, var(--info-color) 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 1.5rem 3rem;
|
||||||
|
border-radius: var(--border-radius-xl);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
transition: all 0.4s var(--ease-premium);
|
||||||
|
box-shadow: var(--shadow-floating);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
min-width: 250px;
|
||||||
|
justify-content: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent );
|
||||||
|
transition: left 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: width 0.6s ease, height 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover::after {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover {
|
||||||
|
background: linear-gradient(135deg, var(--primary-light) 0%, var(--info-color) 50%, var(--success-color) 100%);
|
||||||
|
transform: translateY(-6px) scale(1.05);
|
||||||
|
box-shadow: var(--shadow-ultra);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:active {
|
||||||
|
transform: translateY(-2px) scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit i {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit span {
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validation Styling */
|
||||||
|
.text-danger {
|
||||||
|
color: var(--danger-color);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
display: block;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
border-left: 4px solid var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-validation-error {
|
||||||
|
border-color: var(--danger-color) !important;
|
||||||
|
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.15) !important;
|
||||||
|
animation: shake 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@keyframes shake {
|
||||||
|
0%, 100%
|
||||||
|
|
||||||
|
{
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper Text */
|
||||||
|
.helper-text {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--gray-600);
|
||||||
|
background: rgba(248, 250, 252, 0.8);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
border-left: 3px solid var(--info-color);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@@keyframes slideDown {
|
||||||
|
from
|
||||||
|
|
||||||
|
{
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50px) scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@@keyframes slideUp {
|
||||||
|
from
|
||||||
|
|
||||||
|
{
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(50px) scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert Styling */
|
||||||
|
.alert {
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border: none;
|
||||||
|
box-shadow: var(--shadow-floating);
|
||||||
|
animation: slideDown 0.6s var(--ease-premium);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: linear-gradient(135deg, var(--success-color) 0%, var(--success-light) 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-light) 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@@media (max-width: 768px) {
|
||||||
|
.container
|
||||||
|
|
||||||
|
{
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading States */
|
||||||
|
.btn-submit.loading {
|
||||||
|
background: var(--gray-400);
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit.loading::after {
|
||||||
|
content: "";
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top-color: currentColor;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@keyframes spin {
|
||||||
|
0%
|
||||||
|
|
||||||
|
{
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Success/Error Messages -->
|
||||||
|
@if (TempData["Success"] != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-check-circle-fill"></i> @TempData["Success"]
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (TempData["Error"] != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill"></i> @TempData["Error"]
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Premium Page Header -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-icon">
|
||||||
|
<i class="bi bi-rocket-takeoff-fill"></i>
|
||||||
|
</div>
|
||||||
|
<div class="header-text">
|
||||||
|
<h1>Send Questionnaire</h1>
|
||||||
|
<p class="header-subtitle">Distribute your questionnaire with style and precision to multiple recipients</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Premium Info Cards -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Ultra-Premium Form -->
|
||||||
|
<div class="form-card">
|
||||||
|
<div class="form-header">
|
||||||
|
<div class="form-title">
|
||||||
|
<div class="form-icon">
|
||||||
|
<i class="bi bi-envelope-paper-heart-fill"></i>
|
||||||
|
</div>
|
||||||
|
<h2>Questionnaire Distribution Center</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-body">
|
||||||
<form asp-action="SendQuestionnaire">
|
<form asp-action="SendQuestionnaire">
|
||||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
|
||||||
|
<!-- Questionnaire Selection -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="QuestionnaireId" class="control-label">Questionnaire</label> <!-- Display ViewBag data in the label -->
|
<label asp-for="QuestionnaireId" class="control-label">
|
||||||
<input type="text" class="form-control" value="@ViewBag.questionnaireName" disabled /> <!-- Display ViewBag data in the disabled textbox -->
|
<i class="bi bi-clipboard-data-fill"></i>
|
||||||
<span asp-validation-for="QuestionnaireId" class="text-danger"></span>
|
Selected Questionnaire
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control" value="@ViewBag.questionnaireName" disabled />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Addresses -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="Emails" class="control-label">Email Addresses (separate with commas)</label>
|
<label asp-for="Emails" class="control-label">
|
||||||
<textarea asp-for="Emails" class="form-control"></textarea>
|
<i class="bi bi-people-fill"></i>
|
||||||
<span asp-validation-for="Emails" class="text-danger"></span>
|
Email Recipients
|
||||||
|
</label>
|
||||||
|
<textarea asp-for="Emails"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Enter email addresses separated by commas
|
||||||
|
|
||||||
|
Example:
|
||||||
|
john.doe@company.com, jane.smith@organization.org, user@domain.com
|
||||||
|
|
||||||
|
💡 Tip: You can paste from Excel, CSV, or any text format"
|
||||||
|
rows="5"></textarea>
|
||||||
|
|
||||||
|
<div class="helper-text">
|
||||||
|
<i class="bi bi-info-circle-fill me-2"></i>
|
||||||
|
<strong>Pro Tip:</strong> Copy and paste email lists from Excel, Google Sheets, or any text document. The system will automatically format them correctly.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Expiration Date -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="ExpirationDateTime" class="control-label"></label>
|
<label asp-for="ExpirationDateTime" class="control-label">
|
||||||
<input asp-for="ExpirationDateTime" class="form-control"/>
|
<i class="bi bi-calendar-event-fill"></i>
|
||||||
<span asp-validation-for="ExpirationDateTime" class="text-danger"></span>
|
Survey Expiration
|
||||||
|
</label>
|
||||||
|
<input asp-for="ExpirationDateTime"
|
||||||
|
class="form-control datetime-input"
|
||||||
|
type="datetime-local"
|
||||||
|
min="@DateTime.Now.ToString("yyyy-MM-ddTHH:mm")" />
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<input type="hidden" asp-for="QuestionnaireId" /> <!-- Use hidden input for QuestionnaireId -->
|
<!-- Hidden Fields -->
|
||||||
</div>
|
<input type="hidden" asp-for="QuestionnaireId" />
|
||||||
<div class="form-group">
|
|
||||||
<input type="submit" value="Send" class="btn btn-primary" />
|
<!-- Ultra-Premium Submit Button -->
|
||||||
|
<div class="form-group text-center">
|
||||||
|
<button type="submit" class="btn-submit">
|
||||||
|
<i class="bi bi-send-fill"></i>
|
||||||
|
<span>Send Survey</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Basic form interactions
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
const submitBtn = document.querySelector('.btn-submit');
|
||||||
|
const expirationInput = document.querySelector('input[name="ExpirationDateTime"]');
|
||||||
|
|
||||||
|
// Auto-dismiss alerts
|
||||||
|
const alerts = document.querySelectorAll('.alert');
|
||||||
|
alerts.forEach(alert => {
|
||||||
|
setTimeout(() => {
|
||||||
|
alert.style.opacity = '0';
|
||||||
|
alert.style.transform = 'translateY(-30px) scale(0.95)';
|
||||||
|
setTimeout(() => alert.remove(), 400);
|
||||||
|
}, 6000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form submission with loading state
|
||||||
|
if (form && submitBtn) {
|
||||||
|
form.addEventListener('submit', function() {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.classList.add('loading');
|
||||||
|
submitBtn.innerHTML = '<i class="bi bi-hourglass-split"></i><span>Sending...</span>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default expiration (30 days from now)
|
||||||
|
if (expirationInput && !expirationInput.value) {
|
||||||
|
const defaultDate = new Date();
|
||||||
|
defaultDate.setDate(defaultDate.getDate() + 30);
|
||||||
|
defaultDate.setHours(9, 0, 0, 0);
|
||||||
|
expirationInput.value = defaultDate.toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
574
Web/Areas/Admin/Views/Questionnaire/StatusGuide.cshtml
Normal file
574
Web/Areas/Admin/Views/Questionnaire/StatusGuide.cshtml
Normal file
|
|
@ -0,0 +1,574 @@
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Questionnaire Status Guide";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.status-guide-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-header {
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-header p {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
padding: 2rem;
|
||||||
|
border-right: 1px solid #e2e8f0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-card::before {
|
||||||
|
background: linear-gradient(90deg, #6b7280, #9ca3af);
|
||||||
|
}
|
||||||
|
|
||||||
|
.published-card::before {
|
||||||
|
background: linear-gradient(90deg, #10b981, #34d399);
|
||||||
|
}
|
||||||
|
|
||||||
|
.archived-card::before {
|
||||||
|
background: linear-gradient(90deg, #1f2937, #374151);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-icon {
|
||||||
|
background: linear-gradient(135deg, #6b7280, #9ca3af);
|
||||||
|
}
|
||||||
|
|
||||||
|
.published-icon {
|
||||||
|
background: linear-gradient(135deg, #10b981, #34d399);
|
||||||
|
}
|
||||||
|
|
||||||
|
.archived-icon {
|
||||||
|
background: linear-gradient(135deg, #1f2937, #374151);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-subtitle {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capabilities-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capability-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capability-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capability-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capability-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.can-do {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cannot-do {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limited {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capability-text {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-section {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 2rem;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-steps {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-arrow {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-description {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6b7280;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-section {
|
||||||
|
background: #f0f9ff;
|
||||||
|
border: 1px solid #bae6fd;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-title {
|
||||||
|
color: #0369a1;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-item {
|
||||||
|
color: #0c4a6e;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media (max-width: 768px) {
|
||||||
|
.status-grid
|
||||||
|
|
||||||
|
{
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-steps {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-arrow {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Back Button -->
|
||||||
|
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="status-guide-container">
|
||||||
|
<div class="guide-header">
|
||||||
|
<h1><i class="bi bi-clipboard-check"></i> Questionnaire Status Guide</h1>
|
||||||
|
<p>Complete breakdown of what you can do in each status</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-grid">
|
||||||
|
<!-- DRAFT STATUS -->
|
||||||
|
<div class="status-card draft-card">
|
||||||
|
<div class="status-header">
|
||||||
|
<div class="status-icon draft-icon"><i class="bi bi-pencil"></i></div>
|
||||||
|
<div>
|
||||||
|
<h2 class="status-title">Draft</h2>
|
||||||
|
<p class="status-subtitle">Full editing freedom</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="capabilities-section">
|
||||||
|
<h3 class="section-title"><i class="bi bi-check-circle text-success"></i> What You CAN Do</h3>
|
||||||
|
<ul class="capability-list">
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Edit questionnaire title and description</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Add new questions</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Edit existing questions (text, type)</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Delete questions completely</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Add/edit/delete answers</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Reorder questions</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Delete entire questionnaire</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Publish questionnaire</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="capabilities-section">
|
||||||
|
<h3 class="section-title"><i class="bi bi-x-circle text-danger"></i> What You CANNOT Do</h3>
|
||||||
|
<ul class="capability-list">
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon cannot-do">✗</div>
|
||||||
|
<span class="capability-text">Send to respondents (must publish first)</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon cannot-do">✗</div>
|
||||||
|
<span class="capability-text">Collect responses (not live yet)</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tips-section">
|
||||||
|
<h4 class="tips-title"><i class="bi bi-lightbulb"></i> Best Practices</h4>
|
||||||
|
<div class="tip-item">• Perfect your questions before publishing</div>
|
||||||
|
<div class="tip-item">• Test question flow and logic</div>
|
||||||
|
<div class="tip-item">• Review with team members</div>
|
||||||
|
<div class="tip-item">• Preview how it looks to respondents</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PUBLISHED STATUS -->
|
||||||
|
<div class="status-card published-card">
|
||||||
|
<div class="status-header">
|
||||||
|
<div class="status-icon published-icon"><i class="bi bi-broadcast"></i></div>
|
||||||
|
<div>
|
||||||
|
<h2 class="status-title">Published</h2>
|
||||||
|
<p class="status-subtitle">Live & accepting responses</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="capabilities-section">
|
||||||
|
<h3 class="section-title"><i class="bi bi-check-circle text-success"></i> What You CAN Do</h3>
|
||||||
|
<ul class="capability-list">
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Edit questionnaire title and description</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Edit question text (preserve meaning)</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Add new questions</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Send questionnaire to respondents</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">View responses and analytics</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Archive questionnaire</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="capabilities-section">
|
||||||
|
<h3 class="section-title"><i class="bi bi-exclamation-triangle text-warning"></i> Limited Actions</h3>
|
||||||
|
<ul class="capability-list">
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon limited">⚠</div>
|
||||||
|
<span class="capability-text">Remove questions (hidden, not deleted)</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon limited">⚠</div>
|
||||||
|
<span class="capability-text">Edit answers (only for questions without responses)</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="capabilities-section">
|
||||||
|
<h3 class="section-title"><i class="bi bi-x-circle text-danger"></i> What You CANNOT Do</h3>
|
||||||
|
<ul class="capability-list">
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon cannot-do">✗</div>
|
||||||
|
<span class="capability-text">Delete questionnaire (has responses)</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon cannot-do">✗</div>
|
||||||
|
<span class="capability-text">Permanently delete questions with responses</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon cannot-do">✗</div>
|
||||||
|
<span class="capability-text">Change answers for questions with responses</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon cannot-do">✗</div>
|
||||||
|
<span class="capability-text">Change question types that affect existing responses</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tips-section">
|
||||||
|
<h4 class="tips-title"><i class="bi bi-lightbulb"></i> Best Practices</h4>
|
||||||
|
<div class="tip-item">• Make only essential text changes</div>
|
||||||
|
<div class="tip-item">• Add new questions at the end</div>
|
||||||
|
<div class="tip-item">• Monitor response data regularly</div>
|
||||||
|
<div class="tip-item">• Consider creating a new version for major changes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ARCHIVED STATUS -->
|
||||||
|
<div class="status-card archived-card">
|
||||||
|
<div class="status-header">
|
||||||
|
<div class="status-icon archived-icon"><i class="bi bi-archive"></i></div>
|
||||||
|
<div>
|
||||||
|
<h2 class="status-title">Archived</h2>
|
||||||
|
<p class="status-subtitle">Read-only & complete</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="capabilities-section">
|
||||||
|
<h3 class="section-title"><i class="bi bi-check-circle text-success"></i> What You CAN Do</h3>
|
||||||
|
<ul class="capability-list">
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">View questionnaire details</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">View all responses and data</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Export response data</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Generate reports and analytics</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon can-do">✓</div>
|
||||||
|
<span class="capability-text">Duplicate questionnaire as new draft</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="capabilities-section">
|
||||||
|
<h3 class="section-title"><i class="bi bi-exclamation-triangle text-warning"></i> Limited Actions</h3>
|
||||||
|
<ul class="capability-list">
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon limited">⚠</div>
|
||||||
|
<span class="capability-text">Revert to draft (only if no responses)</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="capabilities-section">
|
||||||
|
<h3 class="section-title"><i class="bi bi-x-circle text-danger"></i> What You CANNOT Do</h3>
|
||||||
|
<ul class="capability-list">
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon cannot-do">✗</div>
|
||||||
|
<span class="capability-text">Edit any questionnaire content</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon cannot-do">✗</div>
|
||||||
|
<span class="capability-text">Add or remove questions</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon cannot-do">✗</div>
|
||||||
|
<span class="capability-text">Send to new respondents</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon cannot-do">✗</div>
|
||||||
|
<span class="capability-text">Collect new responses</span>
|
||||||
|
</li>
|
||||||
|
<li class="capability-item">
|
||||||
|
<div class="capability-icon cannot-do">✗</div>
|
||||||
|
<span class="capability-text">Delete questionnaire (permanent archive)</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tips-section">
|
||||||
|
<h4 class="tips-title"><i class="bi bi-lightbulb"></i> Best Practices</h4>
|
||||||
|
<div class="tip-item">• Export all data before long-term storage</div>
|
||||||
|
<div class="tip-item">• Create final analysis reports</div>
|
||||||
|
<div class="tip-item">• Document key insights and findings</div>
|
||||||
|
<div class="tip-item">• Use as template for future questionnaires</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="workflow-section">
|
||||||
|
<h2 class="workflow-title"><i class="bi bi-arrow-repeat"></i> Status Workflow</h2>
|
||||||
|
<div class="workflow-steps">
|
||||||
|
<div class="workflow-step">
|
||||||
|
<div class="step-icon draft-icon"><i class="bi bi-pencil"></i></div>
|
||||||
|
<div class="step-title">Draft</div>
|
||||||
|
<div class="step-description">Create & perfect your questionnaire</div>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-arrow">→</div>
|
||||||
|
<div class="workflow-step">
|
||||||
|
<div class="step-icon published-icon"><i class="bi bi-broadcast"></i></div>
|
||||||
|
<div class="step-title">Published</div>
|
||||||
|
<div class="step-description">Live & collecting responses</div>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-arrow">→</div>
|
||||||
|
<div class="workflow-step">
|
||||||
|
<div class="step-icon archived-icon"><i class="bi bi-archive"></i></div>
|
||||||
|
<div class="step-title">Archived</div>
|
||||||
|
<div class="step-description">Complete & preserved</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<div class="alert alert-info d-inline-block">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
<strong>Key Benefit:</strong> This status system completely eliminates foreign key constraint errors while providing professional survey management!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -333,42 +333,103 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Response Checkbox - Simplified and Reliable */
|
||||||
.response-checkbox {
|
.response-checkbox {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
z-index: 10;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox {
|
.checkbox-label {
|
||||||
width: 24px;
|
cursor: pointer;
|
||||||
height: 24px;
|
margin: 0;
|
||||||
border: 2px solid var(--gray-300);
|
padding: 0;
|
||||||
border-radius: 6px;
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-checkbox-input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-custom {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid var(--gray-300);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-custom:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-custom i {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-checkbox-input:checked + .checkbox-custom {
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-checkbox-input:checked + .checkbox-custom i {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Master Checkbox - Updated to match */
|
||||||
|
.master-checkbox {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid var(--gray-300);
|
||||||
|
border-radius: 8px;
|
||||||
background: white;
|
background: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox:hover {
|
.master-checkbox:hover {
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15);
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox.checked {
|
.master-checkbox.checked {
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox input {
|
.master-checkbox input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.master-checkbox i {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.response-info {
|
.response-info {
|
||||||
padding-right: 3rem;
|
padding-right: 3rem;
|
||||||
}
|
}
|
||||||
|
|
@ -535,6 +596,7 @@
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn::before {
|
.btn::before {
|
||||||
|
|
@ -576,6 +638,18 @@
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: linear-gradient(135deg, var(--warning-color) 0%, var(--warning-dark) 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover {
|
||||||
|
background: linear-gradient(135deg, var(--warning-dark) 0%, #b45309 100%);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
/* Empty State */
|
/* Empty State */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -673,6 +747,264 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Confirmation Modal Styles */
|
||||||
|
.confirmation-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-modal.show {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--border-radius-xl);
|
||||||
|
box-shadow: var(--shadow-2xl);
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-modal.show .modal-content {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 3rem 2rem 2rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon Row */
|
||||||
|
.modal-icon-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-icon::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-icon.delete-selected {
|
||||||
|
background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-light) 50%, var(--danger-dark) 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 20px 40px rgba(239, 68, 68, 0.4), 0 8px 16px rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-icon.delete-selected::before {
|
||||||
|
background: linear-gradient(135deg, var(--danger-light) 0%, var(--danger-color) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-icon.delete-all {
|
||||||
|
background: linear-gradient(135deg, var(--danger-dark) 0%, var(--danger-color) 50%, #b91c1c 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 20px 40px rgba(220, 38, 38, 0.5), 0 8px 16px rgba(220, 38, 38, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-icon.delete-all::before {
|
||||||
|
background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-dark) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon symbol styling */
|
||||||
|
.modal-icon i {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title Row */
|
||||||
|
.modal-title-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--gray-800);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtitle Row */
|
||||||
|
.modal-subtitle-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-subtitle {
|
||||||
|
color: var(--gray-600);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description {
|
||||||
|
background: var(--gray-50);
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--gray-800);
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
color: var(--gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-info {
|
||||||
|
background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-light) 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-count {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-meta {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--gray-50);
|
||||||
|
border-top: 1px solid var(--gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-cancel {
|
||||||
|
background: white;
|
||||||
|
color: var(--gray-700);
|
||||||
|
border: 2px solid var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-cancel:hover {
|
||||||
|
background: var(--gray-50);
|
||||||
|
border-color: var(--gray-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-confirm {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-confirm.delete-selected {
|
||||||
|
background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-dark) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-confirm.delete-selected:hover {
|
||||||
|
background: linear-gradient(135deg, var(--danger-dark) 0%, #b91c1c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-confirm.delete-all {
|
||||||
|
background: linear-gradient(135deg, var(--danger-dark) 0%, #b91c1c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-confirm.delete-all:hover {
|
||||||
|
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
|
||||||
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
.fade-in {
|
.fade-in {
|
||||||
animation: fadeIn 0.6s ease-out;
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
|
@ -702,10 +1034,35 @@
|
||||||
.stagger-animation:nth-child(6) { animation-delay: 0.6s; }
|
.stagger-animation:nth-child(6) { animation-delay: 0.6s; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container-fluid">
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
<partial name="_Notification" />
|
<partial name="_Notification" />
|
||||||
|
|
||||||
|
<!-- Success/Error Messages -->
|
||||||
|
@if (TempData["Success"] != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-check-circle"></i> @TempData["Success"]
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (TempData["Error"] != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> @TempData["Error"]
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (TempData["Warning"] != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-circle"></i> @TempData["Warning"]
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="page-header fade-in">
|
<div class="page-header fade-in">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
|
|
@ -719,7 +1076,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form asp-action="DeleteMultiple" method="post" id="responseForm">
|
|
||||||
@if (Model.Any())
|
@if (Model.Any())
|
||||||
{
|
{
|
||||||
<!-- Statistics Section -->
|
<!-- Statistics Section -->
|
||||||
|
|
@ -771,7 +1127,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="selection-actions">
|
<div class="selection-actions">
|
||||||
<button type="submit" class="btn btn-danger" id="deleteSelectedBtn">
|
<button type="button" class="btn btn-danger" id="deleteSelectedBtn">
|
||||||
<i class="bi bi-trash-fill"></i>
|
<i class="bi bi-trash-fill"></i>
|
||||||
Delete Selected
|
Delete Selected
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -779,35 +1135,55 @@
|
||||||
<i class="bi bi-x-circle"></i>
|
<i class="bi bi-x-circle"></i>
|
||||||
Deselect All
|
Deselect All
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning" id="deleteAllBtn">
|
||||||
|
<i class="bi bi-trash3-fill"></i>
|
||||||
|
Delete All
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden Forms for Actions -->
|
||||||
|
<form asp-action="DeleteMultiple" method="post" id="deleteMultipleForm" style="display: none;">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<div id="selectedResponsesContainer"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form asp-action="DeleteAll" method="post" id="deleteAllForm" style="display: none;">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Responses Grid -->
|
<!-- Responses Grid -->
|
||||||
<div class="responses-grid">
|
<div class="responses-grid">
|
||||||
@foreach (var item in Model.Select((response, index) => new { Response = response, Index = index }))
|
@foreach (var item in Model.Select((response, index) => new { Response = response, Index = index }))
|
||||||
{
|
{
|
||||||
<div class="response-card stagger-animation" data-id="@item.Response.Id">
|
<div class="response-card stagger-animation" data-id="@item.Response.Id">
|
||||||
<!-- Response Checkbox -->
|
<!-- Response Checkbox - Simplified -->
|
||||||
<div class="response-checkbox">
|
<div class="response-checkbox">
|
||||||
<div class="custom-checkbox response-select-checkbox">
|
<label class="checkbox-label" for="checkbox_@item.Response.Id">
|
||||||
<input type="checkbox" class="response-checkbox-input" name="ids" value="@item.Response.Id">
|
<input type="checkbox"
|
||||||
<i class="bi bi-check" style="display: none;"></i>
|
id="checkbox_@item.Response.Id"
|
||||||
</div>
|
class="response-checkbox-input"
|
||||||
|
value="@item.Response.Id"
|
||||||
|
data-response-id="@item.Response.Id">
|
||||||
|
<span class="checkbox-custom">
|
||||||
|
<i class="bi bi-check"></i>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card Header -->
|
<!-- Card Header -->
|
||||||
<div class="card-header-custom">
|
<div class="card-header-custom">
|
||||||
<div class="response-info">
|
<div class="response-info">
|
||||||
<span class="response-id">ID: @item.Response.Id</span>
|
<span class="response-id">ID: @item.Response.Id</span>
|
||||||
<h3 class="questionnaire-title">@item.Response.Questionnaire?.Title</h3>
|
<h3 class="questionnaire-title">@(item.Response.Questionnaire?.Title ?? "Unknown Questionnaire")</h3>
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="user-avatar">
|
<div class="user-avatar">
|
||||||
@(item.Response.UserName?.Substring(0, 1).ToUpper() ?? "U")
|
@(item.Response.UserName?.Substring(0, 1).ToUpper() ?? "U")
|
||||||
</div>
|
</div>
|
||||||
<div class="user-details">
|
<div class="user-details">
|
||||||
<h4>@item.Response.UserName</h4>
|
<h4>@(item.Response.UserName ?? "Unknown User")</h4>
|
||||||
<p class="user-email">@item.Response.UserEmail</p>
|
<p class="user-email">@(item.Response.UserEmail ?? "No email")</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -850,141 +1226,392 @@
|
||||||
<p>When users start submitting survey responses, they will appear here for you to manage and review.</p>
|
<p>When users start submitting survey responses, they will appear here for you to manage and review.</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirmation Modal -->
|
||||||
|
<div id="confirmationModal" class="confirmation-modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<!-- Icon Row -->
|
||||||
|
<div class="modal-icon-row">
|
||||||
|
<div id="modalIcon" class="modal-icon">
|
||||||
|
<i id="modalIconSymbol" class="bi"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title Row -->
|
||||||
|
<div class="modal-title-row">
|
||||||
|
<h2 id="modalTitle" class="modal-title"></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Subtitle Row -->
|
||||||
|
<div class="modal-subtitle-row">
|
||||||
|
<p id="modalSubtitle" class="modal-subtitle"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="selectionInfo" class="selection-info">
|
||||||
|
<div id="selectionCount" class="selection-count"></div>
|
||||||
|
<div id="selectionMeta" class="selection-meta"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modalDescription" class="modal-description">
|
||||||
|
<h4 id="descriptionTitle"></h4>
|
||||||
|
<ul id="descriptionList"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="button" id="cancelBtn" class="modal-btn modal-btn-cancel">
|
||||||
|
<i class="bi bi-x-circle"></i>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" id="confirmBtn" class="modal-btn modal-btn-confirm">
|
||||||
|
<i id="confirmIcon" class="bi"></i>
|
||||||
|
<span id="confirmText"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
let selectedResponses = [];
|
let selectedResponses = [];
|
||||||
|
|
||||||
|
console.log('Response Management JS Loaded');
|
||||||
|
|
||||||
|
// Modal elements
|
||||||
|
const modal = document.getElementById('confirmationModal');
|
||||||
|
const modalIcon = document.getElementById('modalIcon');
|
||||||
|
const modalIconSymbol = document.getElementById('modalIconSymbol');
|
||||||
|
const modalTitle = document.getElementById('modalTitle');
|
||||||
|
const modalSubtitle = document.getElementById('modalSubtitle');
|
||||||
|
const selectionInfo = document.getElementById('selectionInfo');
|
||||||
|
const selectionCount = document.getElementById('selectionCount');
|
||||||
|
const selectionMeta = document.getElementById('selectionMeta');
|
||||||
|
const modalDescription = document.getElementById('modalDescription');
|
||||||
|
const descriptionTitle = document.getElementById('descriptionTitle');
|
||||||
|
const descriptionList = document.getElementById('descriptionList');
|
||||||
|
const cancelBtn = document.getElementById('cancelBtn');
|
||||||
|
const confirmBtn = document.getElementById('confirmBtn');
|
||||||
|
const confirmIcon = document.getElementById('confirmIcon');
|
||||||
|
const confirmText = document.getElementById('confirmText');
|
||||||
|
|
||||||
|
let currentAction = '';
|
||||||
|
|
||||||
|
// Modal configurations
|
||||||
|
const modalConfigs = {
|
||||||
|
deleteSelected: {
|
||||||
|
icon: 'bi-trash-fill',
|
||||||
|
iconClass: 'delete-selected',
|
||||||
|
title: 'Delete Selected Responses',
|
||||||
|
subtitle: 'Permanently remove the selected survey responses',
|
||||||
|
confirmText: 'Delete Selected',
|
||||||
|
confirmIcon: 'bi-trash-fill',
|
||||||
|
confirmClass: 'delete-selected',
|
||||||
|
descriptionTitle: '<i class="bi bi-exclamation-triangle"></i> What will be deleted:',
|
||||||
|
descriptionItems: [
|
||||||
|
'All selected response records will be permanently removed',
|
||||||
|
'Associated response details and answers will be deleted',
|
||||||
|
'This action cannot be undone or recovered',
|
||||||
|
'Response statistics will be updated automatically'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
deleteAll: {
|
||||||
|
icon: 'bi-trash3-fill',
|
||||||
|
iconClass: 'delete-all',
|
||||||
|
title: 'Delete All Responses',
|
||||||
|
subtitle: 'Permanently remove every survey response in the system',
|
||||||
|
confirmText: 'Delete Everything',
|
||||||
|
confirmIcon: 'bi-trash3-fill',
|
||||||
|
confirmClass: 'delete-all',
|
||||||
|
descriptionTitle: '<i class="bi bi-exclamation-triangle-fill"></i> This will delete:',
|
||||||
|
descriptionItems: [
|
||||||
|
'ALL response records in the entire system',
|
||||||
|
'ALL associated response details and answers',
|
||||||
|
'ALL response statistics and data',
|
||||||
|
'This action cannot be undone - everything will be lost permanently'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function showConfirmationModal(action, count = 0) {
|
||||||
|
currentAction = action;
|
||||||
|
const config = modalConfigs[action];
|
||||||
|
|
||||||
|
// Set modal icon and styling
|
||||||
|
modalIcon.className = `modal-icon ${config.iconClass}`;
|
||||||
|
modalIconSymbol.className = `bi ${config.icon}`;
|
||||||
|
|
||||||
|
// Set modal content
|
||||||
|
modalTitle.textContent = config.title;
|
||||||
|
modalSubtitle.textContent = config.subtitle;
|
||||||
|
|
||||||
|
// Set selection info
|
||||||
|
if (action === 'deleteSelected') {
|
||||||
|
selectionCount.textContent = `${count} Response${count !== 1 ? 's' : ''} Selected`;
|
||||||
|
selectionMeta.textContent = 'These responses will be permanently deleted';
|
||||||
|
} else if (action === 'deleteAll') {
|
||||||
|
selectionCount.textContent = `${count} Total Responses`;
|
||||||
|
selectionMeta.textContent = 'Every response in the system will be deleted';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set description
|
||||||
|
descriptionTitle.innerHTML = config.descriptionTitle;
|
||||||
|
descriptionList.innerHTML = config.descriptionItems.map(item => `<li>${item}</li>`).join('');
|
||||||
|
|
||||||
|
// Set confirm button
|
||||||
|
confirmBtn.className = `modal-btn modal-btn-confirm ${config.confirmClass}`;
|
||||||
|
confirmIcon.className = `bi ${config.confirmIcon}`;
|
||||||
|
confirmText.textContent = config.confirmText;
|
||||||
|
|
||||||
|
// Show modal with animation
|
||||||
|
modal.classList.add('show');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideConfirmationModal() {
|
||||||
|
modal.classList.remove('show');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
|
||||||
|
// Reset after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
currentAction = '';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal event listeners
|
||||||
|
cancelBtn.addEventListener('click', hideConfirmationModal);
|
||||||
|
|
||||||
|
confirmBtn.addEventListener('click', function() {
|
||||||
|
if (currentAction === 'deleteSelected') {
|
||||||
|
// Add loading state
|
||||||
|
this.classList.add('loading');
|
||||||
|
this.disabled = true;
|
||||||
|
|
||||||
|
// Create form with selected IDs
|
||||||
|
const form = document.getElementById('deleteMultipleForm');
|
||||||
|
const container = document.getElementById('selectedResponsesContainer');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
selectedResponses.forEach(function(id) {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = 'ids';
|
||||||
|
input.value = id;
|
||||||
|
container.appendChild(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Submitting delete selected form with IDs:', selectedResponses);
|
||||||
|
form.submit();
|
||||||
|
} else if (currentAction === 'deleteAll') {
|
||||||
|
// Add loading state
|
||||||
|
this.classList.add('loading');
|
||||||
|
this.disabled = true;
|
||||||
|
|
||||||
|
const form = document.getElementById('deleteAllForm');
|
||||||
|
console.log('Submitting delete all form');
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideConfirmationModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
modal.addEventListener('click', function(e) {
|
||||||
|
if (e.target === modal) {
|
||||||
|
hideConfirmationModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal with Escape key
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape' && modal.classList.contains('show')) {
|
||||||
|
hideConfirmationModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent modal content clicks from closing modal
|
||||||
|
modal.querySelector('.modal-content').addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
// Update selection UI
|
// Update selection UI
|
||||||
function updateSelectionUI() {
|
function updateSelectionUI() {
|
||||||
const selectedCount = selectedResponses.length;
|
const selectedCount = selectedResponses.length;
|
||||||
$('#selectedCount').text(selectedCount + ' selected');
|
const selectedCountElement = document.getElementById('selectedCount');
|
||||||
$('#selectionText').text(selectedCount + ' response' + (selectedCount !== 1 ? 's' : '') + ' selected');
|
const selectionTextElement = document.getElementById('selectionText');
|
||||||
|
const selectionControlsElement = document.getElementById('selectionControls');
|
||||||
|
|
||||||
|
console.log('Updating UI, selected count:', selectedCount);
|
||||||
|
|
||||||
|
if (selectedCountElement) {
|
||||||
|
selectedCountElement.textContent = selectedCount + ' selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionTextElement) {
|
||||||
|
selectionTextElement.textContent = selectedCount + ' response' + (selectedCount !== 1 ? 's' : '') + ' selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionControlsElement) {
|
||||||
if (selectedCount > 0) {
|
if (selectedCount > 0) {
|
||||||
$('#selectionControls').addClass('show');
|
selectionControlsElement.classList.add('show');
|
||||||
} else {
|
} else {
|
||||||
$('#selectionControls').removeClass('show');
|
selectionControlsElement.classList.remove('show');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update master checkbox state
|
// Update master checkbox state
|
||||||
const totalResponses = $('.response-checkbox-input').length;
|
const totalResponses = document.querySelectorAll('.response-checkbox-input').length;
|
||||||
const masterCheckbox = $('#selectAllCheckbox');
|
const masterCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
const masterInput = $('#selectAllInput');
|
const masterInput = document.getElementById('selectAllInput');
|
||||||
|
const masterIcon = masterCheckbox ? masterCheckbox.querySelector('i') : null;
|
||||||
|
|
||||||
|
if (masterCheckbox && masterInput && masterIcon) {
|
||||||
if (selectedCount === 0) {
|
if (selectedCount === 0) {
|
||||||
masterCheckbox.removeClass('checked');
|
masterCheckbox.classList.remove('checked');
|
||||||
masterInput.prop('checked', false);
|
masterInput.checked = false;
|
||||||
masterCheckbox.find('i').hide();
|
masterIcon.style.display = 'none';
|
||||||
} else if (selectedCount === totalResponses) {
|
} else if (selectedCount === totalResponses) {
|
||||||
masterCheckbox.addClass('checked');
|
masterCheckbox.classList.add('checked');
|
||||||
masterInput.prop('checked', true);
|
masterInput.checked = true;
|
||||||
masterCheckbox.find('i').show();
|
masterIcon.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
masterCheckbox.removeClass('checked');
|
masterCheckbox.classList.remove('checked');
|
||||||
masterInput.prop('checked', false);
|
masterInput.checked = false;
|
||||||
masterCheckbox.find('i').hide();
|
masterIcon.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle individual response checkbox clicks
|
// Handle individual checkbox changes
|
||||||
$(document).on('change', '.response-checkbox-input', function() {
|
document.addEventListener('change', function(e) {
|
||||||
const responseCard = $(this).closest('.response-card');
|
if (e.target.classList.contains('response-checkbox-input')) {
|
||||||
const responseId = $(this).val();
|
const responseId = parseInt(e.target.value);
|
||||||
const checkbox = $(this).closest('.custom-checkbox');
|
const responseCard = e.target.closest('.response-card');
|
||||||
|
|
||||||
if ($(this).is(':checked')) {
|
console.log('Checkbox changed:', responseId, 'checked:', e.target.checked);
|
||||||
|
|
||||||
|
if (e.target.checked) {
|
||||||
if (!selectedResponses.includes(responseId)) {
|
if (!selectedResponses.includes(responseId)) {
|
||||||
selectedResponses.push(responseId);
|
selectedResponses.push(responseId);
|
||||||
responseCard.addClass('selected');
|
if (responseCard) {
|
||||||
checkbox.addClass('checked');
|
responseCard.classList.add('selected');
|
||||||
checkbox.find('i').show();
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedResponses = selectedResponses.filter(id => id !== responseId);
|
selectedResponses = selectedResponses.filter(id => id !== responseId);
|
||||||
responseCard.removeClass('selected');
|
if (responseCard) {
|
||||||
checkbox.removeClass('checked');
|
responseCard.classList.remove('selected');
|
||||||
checkbox.find('i').hide();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectionUI();
|
updateSelectionUI();
|
||||||
});
|
console.log('Selected responses:', selectedResponses);
|
||||||
|
}
|
||||||
// Handle checkbox visual clicks
|
|
||||||
$(document).on('click', '.response-select-checkbox', function() {
|
|
||||||
const input = $(this).find('input');
|
|
||||||
input.prop('checked', !input.is(':checked')).trigger('change');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle master checkbox
|
// Handle master checkbox
|
||||||
$('#selectAllCheckbox').click(function() {
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
const shouldSelectAll = selectedResponses.length !== $('.response-checkbox-input').length;
|
if (selectAllCheckbox) {
|
||||||
|
selectAllCheckbox.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
$('.response-checkbox-input').each(function() {
|
const allInputs = document.querySelectorAll('.response-checkbox-input');
|
||||||
const responseCard = $(this).closest('.response-card');
|
const shouldSelectAll = selectedResponses.length !== allInputs.length;
|
||||||
const responseId = $(this).val();
|
|
||||||
const checkbox = $(this).closest('.custom-checkbox');
|
|
||||||
|
|
||||||
$(this).prop('checked', shouldSelectAll);
|
console.log('Master checkbox clicked, shouldSelectAll:', shouldSelectAll);
|
||||||
|
|
||||||
if (shouldSelectAll) {
|
if (shouldSelectAll) {
|
||||||
if (!selectedResponses.includes(responseId)) {
|
selectedResponses = [];
|
||||||
|
allInputs.forEach(function(input) {
|
||||||
|
const responseId = parseInt(input.value);
|
||||||
|
const responseCard = input.closest('.response-card');
|
||||||
|
|
||||||
|
input.checked = true;
|
||||||
selectedResponses.push(responseId);
|
selectedResponses.push(responseId);
|
||||||
|
if (responseCard) {
|
||||||
|
responseCard.classList.add('selected');
|
||||||
}
|
}
|
||||||
responseCard.addClass('selected');
|
});
|
||||||
checkbox.addClass('checked');
|
|
||||||
checkbox.find('i').show();
|
|
||||||
} else {
|
} else {
|
||||||
selectedResponses = [];
|
selectedResponses = [];
|
||||||
responseCard.removeClass('selected');
|
allInputs.forEach(function(input) {
|
||||||
checkbox.removeClass('checked');
|
const responseCard = input.closest('.response-card');
|
||||||
checkbox.find('i').hide();
|
|
||||||
|
input.checked = false;
|
||||||
|
if (responseCard) {
|
||||||
|
responseCard.classList.remove('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectionUI();
|
||||||
|
console.log('Updated selected responses:', selectedResponses);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deselect all button
|
||||||
|
const deselectAllBtn = document.getElementById('deselectAllBtn');
|
||||||
|
if (deselectAllBtn) {
|
||||||
|
deselectAllBtn.addEventListener('click', function() {
|
||||||
|
console.log('Deselect all clicked');
|
||||||
|
selectedResponses = [];
|
||||||
|
|
||||||
|
document.querySelectorAll('.response-checkbox-input').forEach(function(input) {
|
||||||
|
const responseCard = input.closest('.response-card');
|
||||||
|
input.checked = false;
|
||||||
|
if (responseCard) {
|
||||||
|
responseCard.classList.remove('selected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
updateSelectionUI();
|
updateSelectionUI();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Deselect all button
|
// Delete selected button with beautiful modal
|
||||||
$('#deselectAllBtn').click(function() {
|
const deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
|
||||||
selectedResponses = [];
|
if (deleteSelectedBtn) {
|
||||||
$('.response-checkbox-input').prop('checked', false);
|
deleteSelectedBtn.addEventListener('click', function() {
|
||||||
$('.response-card').removeClass('selected');
|
console.log('Delete selected clicked. Selected responses:', selectedResponses);
|
||||||
$('.custom-checkbox').removeClass('checked');
|
|
||||||
$('.custom-checkbox i').hide();
|
|
||||||
updateSelectionUI();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete selected button with confirmation
|
|
||||||
$('#deleteSelectedBtn').click(function(e) {
|
|
||||||
if (selectedResponses.length === 0) {
|
if (selectedResponses.length === 0) {
|
||||||
e.preventDefault();
|
|
||||||
alert('Please select at least one response to delete.');
|
alert('Please select at least one response to delete.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmMessage = `Are you sure you want to delete ${selectedResponses.length} response${selectedResponses.length !== 1 ? 's' : ''}? This action cannot be undone.`;
|
showConfirmationModal('deleteSelected', selectedResponses.length);
|
||||||
|
});
|
||||||
if (!confirm(confirmMessage)) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add loading state
|
// Delete all button with beautiful modal
|
||||||
$(this).addClass('loading').prop('disabled', true);
|
const deleteAllBtn = document.getElementById('deleteAllBtn');
|
||||||
|
if (deleteAllBtn) {
|
||||||
|
deleteAllBtn.addEventListener('click', function() {
|
||||||
|
const totalResponses = document.querySelectorAll('.response-checkbox-input').length;
|
||||||
|
showConfirmationModal('deleteAll', totalResponses);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize UI
|
// Initialize UI
|
||||||
updateSelectionUI();
|
updateSelectionUI();
|
||||||
});
|
|
||||||
|
|
||||||
// Legacy function for backward compatibility
|
// Debug info
|
||||||
function selectAll(source) {
|
const checkboxes = document.querySelectorAll('.response-checkbox-input');
|
||||||
const event = new Event('click');
|
console.log('Total checkboxes found:', checkboxes.length);
|
||||||
document.getElementById('selectAllCheckbox').dispatchEvent(event);
|
|
||||||
}
|
// Test each checkbox
|
||||||
|
checkboxes.forEach((checkbox, index) => {
|
||||||
|
console.log(`Checkbox ${index}:`, {
|
||||||
|
id: checkbox.id,
|
||||||
|
value: checkbox.value,
|
||||||
|
visible: checkbox.offsetParent !== null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
@ -51,8 +51,6 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Header Section */
|
/* Header Section */
|
||||||
.page-header {
|
.page-header {
|
||||||
background: white;
|
background: white;
|
||||||
|
|
@ -236,10 +234,11 @@
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Master Checkbox - Updated to match response page */
|
||||||
.master-checkbox {
|
.master-checkbox {
|
||||||
width: 28px;
|
width: 32px;
|
||||||
height: 28px;
|
height: 32px;
|
||||||
border: 2px solid var(--gray-300);
|
border: 3px solid var(--gray-300);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: white;
|
background: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -247,12 +246,13 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.master-checkbox:hover {
|
.master-checkbox:hover {
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15);
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.master-checkbox.checked {
|
.master-checkbox.checked {
|
||||||
|
|
@ -265,6 +265,11 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.master-checkbox i {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
/* User Cards Grid */
|
/* User Cards Grid */
|
||||||
.users-grid {
|
.users-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -326,40 +331,65 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* User Checkbox - Simplified and Reliable */
|
||||||
.user-checkbox {
|
.user-checkbox {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
z-index: 10;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox {
|
.checkbox-label {
|
||||||
width: 24px;
|
cursor: pointer;
|
||||||
height: 24px;
|
margin: 0;
|
||||||
border: 2px solid var(--gray-300);
|
padding: 0;
|
||||||
border-radius: 6px;
|
display: block;
|
||||||
background: white;
|
}
|
||||||
|
|
||||||
|
.user-checkbox-input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-custom {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid var(--gray-300);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-md);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox:hover {
|
.checkbox-custom:hover {
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15);
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox.checked {
|
.checkbox-custom i {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-checkbox-input:checked + .checkbox-custom {
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-checkbox input {
|
.user-checkbox-input:checked + .checkbox-custom i {
|
||||||
display: none;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
|
|
@ -527,6 +557,7 @@
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn::before {
|
.btn::before {
|
||||||
|
|
@ -568,6 +599,236 @@
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Confirmation Modal Styles */
|
||||||
|
.confirmation-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-modal.show {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--border-radius-xl);
|
||||||
|
box-shadow: var(--shadow-2xl);
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-modal.show .modal-content {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 3rem 2rem 2rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon Row */
|
||||||
|
.modal-icon-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-light) 50%, var(--danger-dark) 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 20px 40px rgba(239, 68, 68, 0.4), 0 8px 16px rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-icon::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.3;
|
||||||
|
background: linear-gradient(135deg, var(--danger-light) 0%, var(--danger-color) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-icon i {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title Row */
|
||||||
|
.modal-title-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--gray-800);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtitle Row */
|
||||||
|
.modal-subtitle-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-subtitle {
|
||||||
|
color: var(--gray-600);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description {
|
||||||
|
background: var(--gray-50);
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--gray-800);
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
color: var(--gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-info {
|
||||||
|
background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-light) 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-count {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-meta {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--gray-50);
|
||||||
|
border-top: 1px solid var(--gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-cancel {
|
||||||
|
background: white;
|
||||||
|
color: var(--gray-700);
|
||||||
|
border: 2px solid var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-cancel:hover {
|
||||||
|
background: var(--gray-50);
|
||||||
|
border-color: var(--gray-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-confirm {
|
||||||
|
background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-dark) 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-confirm:hover {
|
||||||
|
background: linear-gradient(135deg, var(--danger-dark) 0%, #b91c1c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
/* Empty State */
|
/* Empty State */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -657,6 +918,20 @@
|
||||||
.selection-actions .btn {
|
.selection-actions .btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-body,
|
||||||
|
.modal-actions {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
|
|
@ -689,6 +964,34 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
<!-- Notifications -->
|
||||||
|
<partial name="_Notification" />
|
||||||
|
|
||||||
|
<!-- Success/Error Messages -->
|
||||||
|
@if (TempData["Success"] != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-check-circle"></i> @TempData["Success"]
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (TempData["Error"] != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> @TempData["Error"]
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (TempData["Warning"] != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-circle"></i> @TempData["Warning"]
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="page-header fade-in">
|
<div class="page-header fade-in">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
|
|
@ -702,7 +1005,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form asp-action="DeleteSelected" method="post" id="responseForm">
|
|
||||||
@if (Model.Any())
|
@if (Model.Any())
|
||||||
{
|
{
|
||||||
<!-- Statistics Section -->
|
<!-- Statistics Section -->
|
||||||
|
|
@ -754,7 +1056,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="selection-actions">
|
<div class="selection-actions">
|
||||||
<button type="submit" class="btn btn-danger" id="deleteSelectedBtn">
|
<button type="button" class="btn btn-danger" id="deleteSelectedBtn">
|
||||||
<i class="bi bi-trash-fill"></i>
|
<i class="bi bi-trash-fill"></i>
|
||||||
Delete Selected
|
Delete Selected
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -766,28 +1068,40 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden Form for Deletion -->
|
||||||
|
<form asp-action="DeleteSelected" method="post" id="deleteSelectedForm" style="display: none;">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<div id="selectedEmailsContainer"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Users Grid -->
|
<!-- Users Grid -->
|
||||||
<div class="users-grid">
|
<div class="users-grid">
|
||||||
@foreach (var item in Model.Select((user, index) => new { User = user, Index = index }))
|
@foreach (var item in Model.Select((user, index) => new { User = user, Index = index }))
|
||||||
{
|
{
|
||||||
<div class="user-card stagger-animation" data-email="@item.User.UserEmail">
|
<div class="user-card stagger-animation" data-email="@item.User.UserEmail">
|
||||||
<!-- Card Header -->
|
|
||||||
<div class="card-header-custom">
|
|
||||||
<!-- User Checkbox -->
|
<!-- User Checkbox -->
|
||||||
<div class="user-checkbox">
|
<div class="user-checkbox">
|
||||||
<div class="custom-checkbox user-select-checkbox">
|
<label class="checkbox-label" for="checkbox_@item.Index">
|
||||||
<input type="checkbox" class="selectCheckbox" name="selectedEmails" value="@item.User.UserEmail">
|
<input type="checkbox"
|
||||||
<i class="bi bi-check" style="display: none;"></i>
|
id="checkbox_@item.Index"
|
||||||
</div>
|
class="user-checkbox-input"
|
||||||
|
value="@item.User.UserEmail"
|
||||||
|
data-user-email="@item.User.UserEmail">
|
||||||
|
<span class="checkbox-custom">
|
||||||
|
<i class="bi bi-check"></i>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Card Header -->
|
||||||
|
<div class="card-header-custom">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="user-avatar">
|
<div class="user-avatar">
|
||||||
@(item.User.UserName?.Substring(0, 1).ToUpper() ?? "U")
|
@(item.User.UserName?.Substring(0, 1).ToUpper() ?? "U")
|
||||||
</div>
|
</div>
|
||||||
<div class="user-details">
|
<div class="user-details">
|
||||||
<h3>@item.User.UserName</h3>
|
<h3>@(item.User.UserName ?? "Unknown User")</h3>
|
||||||
<p class="user-email">@item.User.UserEmail</p>
|
<p class="user-email">@(item.User.UserEmail ?? "No email")</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -809,7 +1123,7 @@
|
||||||
<div class="surveys-grid">
|
<div class="surveys-grid">
|
||||||
@foreach (var response in item.User.Responses)
|
@foreach (var response in item.User.Responses)
|
||||||
{
|
{
|
||||||
<span class="survey-badge">@response.Questionnaire?.Title</span>
|
<span class="survey-badge">@(response.Questionnaire?.Title ?? "Unknown Survey")</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -827,7 +1141,7 @@
|
||||||
<div class="action-section">
|
<div class="action-section">
|
||||||
<a asp-controller="UserResponseStatus"
|
<a asp-controller="UserResponseStatus"
|
||||||
asp-action="UserResponsesStatus"
|
asp-action="UserResponsesStatus"
|
||||||
asp-route-UserEmail="@item.User.UserEmail"
|
asp-route-userEmail="@item.User.UserEmail"
|
||||||
class="action-btn">
|
class="action-btn">
|
||||||
<i class="bi bi-eye-fill"></i>
|
<i class="bi bi-eye-fill"></i>
|
||||||
View Response Details
|
View Response Details
|
||||||
|
|
@ -848,135 +1162,297 @@
|
||||||
<p>When users start responding to surveys, they will appear here for you to manage and review.</p>
|
<p>When users start responding to surveys, they will appear here for you to manage and review.</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirmation Modal -->
|
||||||
|
<div id="confirmationModal" class="confirmation-modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<!-- Icon Row -->
|
||||||
|
<div class="modal-icon-row">
|
||||||
|
<div class="modal-icon">
|
||||||
|
<i class="bi bi-trash-fill"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title Row -->
|
||||||
|
<div class="modal-title-row">
|
||||||
|
<h2 class="modal-title">Delete User Responses</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Subtitle Row -->
|
||||||
|
<div class="modal-subtitle-row">
|
||||||
|
<p class="modal-subtitle">Permanently remove all survey responses from selected users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="selection-info">
|
||||||
|
<div id="selectionCountModal" class="selection-count"></div>
|
||||||
|
<div class="selection-meta">All responses from these users will be permanently deleted</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-description">
|
||||||
|
<h4><i class="bi bi-exclamation-triangle"></i> What will be deleted:</h4>
|
||||||
|
<ul>
|
||||||
|
<li>All survey response records from selected users</li>
|
||||||
|
<li>Associated response details and answers will be removed</li>
|
||||||
|
<li>User participation history will be lost</li>
|
||||||
|
<li>This action cannot be undone or recovered</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="button" id="cancelBtn" class="modal-btn modal-btn-cancel">
|
||||||
|
<i class="bi bi-x-circle"></i>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" id="confirmBtn" class="modal-btn modal-btn-confirm">
|
||||||
|
<i class="bi bi-trash-fill"></i>
|
||||||
|
Delete User Responses
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
let selectedUsers = [];
|
let selectedUsers = [];
|
||||||
|
|
||||||
|
console.log('User Responses Management JS Loaded');
|
||||||
|
|
||||||
|
// Modal elements
|
||||||
|
const modal = document.getElementById('confirmationModal');
|
||||||
|
const selectionCountModal = document.getElementById('selectionCountModal');
|
||||||
|
const cancelBtn = document.getElementById('cancelBtn');
|
||||||
|
const confirmBtn = document.getElementById('confirmBtn');
|
||||||
|
|
||||||
|
function showConfirmationModal(count) {
|
||||||
|
selectionCountModal.textContent = `${count} User${count !== 1 ? 's' : ''} Selected`;
|
||||||
|
|
||||||
|
// Show modal with animation
|
||||||
|
modal.classList.add('show');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideConfirmationModal() {
|
||||||
|
modal.classList.remove('show');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal event listeners
|
||||||
|
cancelBtn.addEventListener('click', hideConfirmationModal);
|
||||||
|
|
||||||
|
confirmBtn.addEventListener('click', function() {
|
||||||
|
// Add loading state
|
||||||
|
this.classList.add('loading');
|
||||||
|
this.disabled = true;
|
||||||
|
|
||||||
|
// Create form with selected emails
|
||||||
|
const form = document.getElementById('deleteSelectedForm');
|
||||||
|
const container = document.getElementById('selectedEmailsContainer');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
selectedUsers.forEach(function(email) {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = 'selectedEmails';
|
||||||
|
input.value = email;
|
||||||
|
container.appendChild(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Submitting form with emails:', selectedUsers);
|
||||||
|
form.submit();
|
||||||
|
|
||||||
|
hideConfirmationModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
modal.addEventListener('click', function(e) {
|
||||||
|
if (e.target === modal) {
|
||||||
|
hideConfirmationModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal with Escape key
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape' && modal.classList.contains('show')) {
|
||||||
|
hideConfirmationModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent modal content clicks from closing modal
|
||||||
|
modal.querySelector('.modal-content').addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
// Update selection UI
|
// Update selection UI
|
||||||
function updateSelectionUI() {
|
function updateSelectionUI() {
|
||||||
const selectedCount = selectedUsers.length;
|
const selectedCount = selectedUsers.length;
|
||||||
$('#selectedCount').text(selectedCount + ' selected');
|
const selectedCountElement = document.getElementById('selectedCount');
|
||||||
$('#selectionText').text(selectedCount + ' user' + (selectedCount !== 1 ? 's' : '') + ' selected');
|
const selectionTextElement = document.getElementById('selectionText');
|
||||||
|
const selectionControlsElement = document.getElementById('selectionControls');
|
||||||
|
|
||||||
|
console.log('Updating UI, selected count:', selectedCount);
|
||||||
|
|
||||||
|
if (selectedCountElement) {
|
||||||
|
selectedCountElement.textContent = selectedCount + ' selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionTextElement) {
|
||||||
|
selectionTextElement.textContent = selectedCount + ' user' + (selectedCount !== 1 ? 's' : '') + ' selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionControlsElement) {
|
||||||
if (selectedCount > 0) {
|
if (selectedCount > 0) {
|
||||||
$('#selectionControls').addClass('show');
|
selectionControlsElement.classList.add('show');
|
||||||
} else {
|
} else {
|
||||||
$('#selectionControls').removeClass('show');
|
selectionControlsElement.classList.remove('show');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update master checkbox state
|
// Update master checkbox state
|
||||||
const totalUsers = $('.selectCheckbox').length;
|
const totalUsers = document.querySelectorAll('.user-checkbox-input').length;
|
||||||
const masterCheckbox = $('#selectAllCheckbox');
|
const masterCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
const masterInput = $('#selectAllInput');
|
const masterInput = document.getElementById('selectAllInput');
|
||||||
|
const masterIcon = masterCheckbox ? masterCheckbox.querySelector('i') : null;
|
||||||
|
|
||||||
|
if (masterCheckbox && masterInput && masterIcon) {
|
||||||
if (selectedCount === 0) {
|
if (selectedCount === 0) {
|
||||||
masterCheckbox.removeClass('checked');
|
masterCheckbox.classList.remove('checked');
|
||||||
masterInput.prop('checked', false);
|
masterInput.checked = false;
|
||||||
masterCheckbox.find('i').hide();
|
masterIcon.style.display = 'none';
|
||||||
} else if (selectedCount === totalUsers) {
|
} else if (selectedCount === totalUsers) {
|
||||||
masterCheckbox.addClass('checked');
|
masterCheckbox.classList.add('checked');
|
||||||
masterInput.prop('checked', true);
|
masterInput.checked = true;
|
||||||
masterCheckbox.find('i').show();
|
masterIcon.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
masterCheckbox.removeClass('checked');
|
masterCheckbox.classList.remove('checked');
|
||||||
masterInput.prop('checked', false);
|
masterInput.checked = false;
|
||||||
masterCheckbox.find('i').hide();
|
masterIcon.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle individual user checkbox clicks
|
// Handle individual checkbox changes
|
||||||
$(document).on('change', '.selectCheckbox', function() {
|
document.addEventListener('change', function(e) {
|
||||||
const userCard = $(this).closest('.user-card');
|
if (e.target.classList.contains('user-checkbox-input')) {
|
||||||
const userEmail = $(this).val();
|
const userEmail = e.target.value;
|
||||||
const checkbox = $(this).closest('.custom-checkbox');
|
const userCard = e.target.closest('.user-card');
|
||||||
|
|
||||||
if ($(this).is(':checked')) {
|
console.log('Checkbox changed:', userEmail, 'checked:', e.target.checked);
|
||||||
|
|
||||||
|
if (e.target.checked) {
|
||||||
if (!selectedUsers.includes(userEmail)) {
|
if (!selectedUsers.includes(userEmail)) {
|
||||||
selectedUsers.push(userEmail);
|
selectedUsers.push(userEmail);
|
||||||
userCard.addClass('selected');
|
if (userCard) {
|
||||||
checkbox.addClass('checked');
|
userCard.classList.add('selected');
|
||||||
checkbox.find('i').show();
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedUsers = selectedUsers.filter(email => email !== userEmail);
|
selectedUsers = selectedUsers.filter(email => email !== userEmail);
|
||||||
userCard.removeClass('selected');
|
if (userCard) {
|
||||||
checkbox.removeClass('checked');
|
userCard.classList.remove('selected');
|
||||||
checkbox.find('i').hide();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectionUI();
|
updateSelectionUI();
|
||||||
});
|
console.log('Selected users:', selectedUsers);
|
||||||
|
}
|
||||||
// Handle checkbox visual clicks
|
|
||||||
$(document).on('click', '.user-select-checkbox', function() {
|
|
||||||
const input = $(this).find('input');
|
|
||||||
input.prop('checked', !input.is(':checked')).trigger('change');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle master checkbox
|
// Handle master checkbox
|
||||||
$('#selectAllCheckbox').click(function() {
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
const shouldSelectAll = selectedUsers.length !== $('.selectCheckbox').length;
|
if (selectAllCheckbox) {
|
||||||
|
selectAllCheckbox.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
$('.selectCheckbox').each(function() {
|
const allInputs = document.querySelectorAll('.user-checkbox-input');
|
||||||
const userCard = $(this).closest('.user-card');
|
const shouldSelectAll = selectedUsers.length !== allInputs.length;
|
||||||
const userEmail = $(this).val();
|
|
||||||
const checkbox = $(this).closest('.custom-checkbox');
|
|
||||||
|
|
||||||
$(this).prop('checked', shouldSelectAll);
|
console.log('Master checkbox clicked, shouldSelectAll:', shouldSelectAll);
|
||||||
|
|
||||||
if (shouldSelectAll) {
|
if (shouldSelectAll) {
|
||||||
if (!selectedUsers.includes(userEmail)) {
|
selectedUsers = [];
|
||||||
|
allInputs.forEach(function(input) {
|
||||||
|
const userEmail = input.value;
|
||||||
|
const userCard = input.closest('.user-card');
|
||||||
|
|
||||||
|
input.checked = true;
|
||||||
selectedUsers.push(userEmail);
|
selectedUsers.push(userEmail);
|
||||||
|
if (userCard) {
|
||||||
|
userCard.classList.add('selected');
|
||||||
}
|
}
|
||||||
userCard.addClass('selected');
|
});
|
||||||
checkbox.addClass('checked');
|
|
||||||
checkbox.find('i').show();
|
|
||||||
} else {
|
} else {
|
||||||
selectedUsers = [];
|
selectedUsers = [];
|
||||||
userCard.removeClass('selected');
|
allInputs.forEach(function(input) {
|
||||||
checkbox.removeClass('checked');
|
const userCard = input.closest('.user-card');
|
||||||
checkbox.find('i').hide();
|
|
||||||
|
input.checked = false;
|
||||||
|
if (userCard) {
|
||||||
|
userCard.classList.remove('selected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateSelectionUI();
|
updateSelectionUI();
|
||||||
|
console.log('Updated selected users:', selectedUsers);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Deselect all button
|
// Deselect all button
|
||||||
$('#deselectAllBtn').click(function() {
|
const deselectAllBtn = document.getElementById('deselectAllBtn');
|
||||||
|
if (deselectAllBtn) {
|
||||||
|
deselectAllBtn.addEventListener('click', function() {
|
||||||
|
console.log('Deselect all clicked');
|
||||||
selectedUsers = [];
|
selectedUsers = [];
|
||||||
$('.selectCheckbox').prop('checked', false);
|
|
||||||
$('.user-card').removeClass('selected');
|
document.querySelectorAll('.user-checkbox-input').forEach(function(input) {
|
||||||
$('.custom-checkbox').removeClass('checked');
|
const userCard = input.closest('.user-card');
|
||||||
$('.custom-checkbox i').hide();
|
input.checked = false;
|
||||||
|
if (userCard) {
|
||||||
|
userCard.classList.remove('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
updateSelectionUI();
|
updateSelectionUI();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete selected button with beautiful modal
|
||||||
|
const deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
|
||||||
|
if (deleteSelectedBtn) {
|
||||||
|
deleteSelectedBtn.addEventListener('click', function() {
|
||||||
|
console.log('Delete selected clicked. Selected users:', selectedUsers);
|
||||||
|
|
||||||
// Delete selected button with confirmation
|
|
||||||
$('#deleteSelectedBtn').click(function(e) {
|
|
||||||
if (selectedUsers.length === 0) {
|
if (selectedUsers.length === 0) {
|
||||||
e.preventDefault();
|
alert('Please select at least one user to delete responses for.');
|
||||||
alert('Please select at least one user to delete.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmMessage = `Are you sure you want to delete ${selectedUsers.length} user response${selectedUsers.length !== 1 ? 's' : ''}? This action cannot be undone.`;
|
showConfirmationModal(selectedUsers.length);
|
||||||
|
|
||||||
if (!confirm(confirmMessage)) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add loading state
|
|
||||||
$(this).addClass('loading').prop('disabled', true);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize UI
|
// Initialize UI
|
||||||
updateSelectionUI();
|
updateSelectionUI();
|
||||||
|
|
||||||
|
// Debug info
|
||||||
|
const checkboxes = document.querySelectorAll('.user-checkbox-input');
|
||||||
|
console.log('Total checkboxes found:', checkboxes.length);
|
||||||
|
|
||||||
|
// Test each checkbox
|
||||||
|
checkboxes.forEach((checkbox, index) => {
|
||||||
|
console.log(`Checkbox ${index}:`, {
|
||||||
|
id: checkbox.id,
|
||||||
|
value: checkbox.value,
|
||||||
|
visible: checkbox.offsetParent !== null
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
865
Web/Migrations/20250816123754_AddQuestionnaireStatusAndSoftDelete.Designer.cs
generated
Normal file
865
Web/Migrations/20250816123754_AddQuestionnaireStatusAndSoftDelete.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,865 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Web.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SurveyContext))]
|
||||||
|
[Migration("20250816123754_AddQuestionnaireStatusAndSoftDelete")]
|
||||||
|
partial class AddQuestionnaireStatusAndSoftDelete
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.4")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex")
|
||||||
|
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Address", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("CVR")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("City")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Country")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Mobile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PostalCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Street")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Addresss");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Answer", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ConditionJson")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsOtherOption")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("QuestionId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("QuestionId");
|
||||||
|
|
||||||
|
b.ToTable("Answers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.ApplicationUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("FirstName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("LastName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex")
|
||||||
|
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Banner", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ImageUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("LinkUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Banners");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Footer", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ImageUlr")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdated")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Owner")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Sitecopyright")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UpdatedBy")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Footers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.FooterSocialMedia", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("FooterId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SocialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("FooterId", "SocialId");
|
||||||
|
|
||||||
|
b.HasIndex("SocialId");
|
||||||
|
|
||||||
|
b.ToTable("FooterSocialMedias");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Page", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("BannerId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("FooterId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BannerId");
|
||||||
|
|
||||||
|
b.HasIndex("FooterId");
|
||||||
|
|
||||||
|
b.ToTable("Pages");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Question", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("QuestionnaireId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("QuestionnaireId");
|
||||||
|
|
||||||
|
b.ToTable("Questions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Questionnaire", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ArchivedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PublishedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Questionnaires");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Response", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("QuestionnaireId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SubmissionDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("UserEmail")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("QuestionnaireId");
|
||||||
|
|
||||||
|
b.ToTable("Responses");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.ResponseAnswer", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("AnswerId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("ResponseDetailId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ResponseDetailId");
|
||||||
|
|
||||||
|
b.ToTable("ResponseAnswers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.ResponseDetail", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("OtherText")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("QuestionId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("QuestionType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("ResponseId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("SkipReason")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("TextResponse")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("QuestionId");
|
||||||
|
|
||||||
|
b.HasIndex("ResponseId");
|
||||||
|
|
||||||
|
b.ToTable("ResponseDetails");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.SentNewsletterEamil", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Body")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Geo")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsBlocked")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsBounced")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsClicked")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDelivered")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsOpened")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSent")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSpam")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUnsubscribed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ReceivedActivity")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("RecipientEmail")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SentDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Subject")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("SentNewsletterEamils");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.SocialMedia", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("SocialMedia");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Subscription", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSubscribed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Subscriptions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Model.ApplicationUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Answer", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Model.Question", "Question")
|
||||||
|
.WithMany("Answers")
|
||||||
|
.HasForeignKey("QuestionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Question");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.FooterSocialMedia", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Model.Footer", "Footer")
|
||||||
|
.WithMany("FooterSocialMedias")
|
||||||
|
.HasForeignKey("FooterId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Model.SocialMedia", "SocialMedia")
|
||||||
|
.WithMany("FooterSocialMedias")
|
||||||
|
.HasForeignKey("SocialId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Footer");
|
||||||
|
|
||||||
|
b.Navigation("SocialMedia");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Page", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Model.Banner", "banner")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BannerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Model.Footer", "footer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FooterId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("banner");
|
||||||
|
|
||||||
|
b.Navigation("footer");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Question", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Model.Questionnaire", "Questionnaire")
|
||||||
|
.WithMany("Questions")
|
||||||
|
.HasForeignKey("QuestionnaireId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Questionnaire");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Response", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Model.Questionnaire", "Questionnaire")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("QuestionnaireId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Questionnaire");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.ResponseAnswer", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Model.ResponseDetail", "ResponseDetail")
|
||||||
|
.WithMany("ResponseAnswers")
|
||||||
|
.HasForeignKey("ResponseDetailId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("ResponseDetail");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.ResponseDetail", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Model.Question", "Question")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("QuestionId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Model.Response", "Response")
|
||||||
|
.WithMany("ResponseDetails")
|
||||||
|
.HasForeignKey("ResponseId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Question");
|
||||||
|
|
||||||
|
b.Navigation("Response");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Footer", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("FooterSocialMedias");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Question", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Answers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Questionnaire", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Questions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.Response", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ResponseDetails");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.ResponseDetail", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ResponseAnswers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Model.SocialMedia", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("FooterSocialMedias");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Web.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddQuestionnaireStatusAndSoftDelete : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "CreatedDate",
|
||||||
|
table: "Questions",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsActive",
|
||||||
|
table: "Questions",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "ArchivedDate",
|
||||||
|
table: "Questionnaires",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "CreatedDate",
|
||||||
|
table: "Questionnaires",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "PublishedDate",
|
||||||
|
table: "Questionnaires",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Status",
|
||||||
|
table: "Questionnaires",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CreatedDate",
|
||||||
|
table: "Questions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsActive",
|
||||||
|
table: "Questions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ArchivedDate",
|
||||||
|
table: "Questionnaires");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CreatedDate",
|
||||||
|
table: "Questionnaires");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PublishedDate",
|
||||||
|
table: "Questionnaires");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Status",
|
||||||
|
table: "Questionnaires");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -434,6 +434,12 @@ namespace Web.Migrations
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<int>("QuestionnaireId")
|
b.Property<int>("QuestionnaireId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
|
@ -458,9 +464,21 @@ namespace Web.Migrations
|
||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ArchivedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PublishedDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,15 @@ namespace Web.ViewModel.QuestionnaireVM
|
||||||
public List<Answer>? Answers { get; set; }
|
public List<Answer>? Answers { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
// ADD THESE NEW PROPERTIES (for status management):
|
||||||
|
public QuestionnaireStatus Status { get; set; } = QuestionnaireStatus.Draft;
|
||||||
|
public DateTime CreatedDate { get; set; }
|
||||||
|
public DateTime? PublishedDate { get; set; }
|
||||||
|
public DateTime? ArchivedDate { get; set; }
|
||||||
|
|
||||||
|
// Helper properties for the view
|
||||||
|
public int ActiveQuestionCount => Questions?.Count(q => q.IsActive) ?? 0;
|
||||||
|
public bool HasResponses { get; set; } // We'll set this in the controller
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -193,26 +193,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Other Option Styling */
|
/* Other Option Styling */
|
||||||
.other-option-container {
|
|
||||||
position: relative;
|
|
||||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.05) 100%);
|
|
||||||
border: 1px solid rgba(245, 158, 11, 0.2);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.other-option-badge {
|
|
||||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-left: 8px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.other-text-container {
|
.other-text-container {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|
@ -1078,7 +1061,7 @@
|
||||||
@answer.Text
|
@answer.Text
|
||||||
@if (answer.IsOtherOption)
|
@if (answer.IsOtherOption)
|
||||||
{
|
{
|
||||||
<span class="other-option-badge">Other Option</span>
|
|
||||||
}
|
}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
@ -1115,7 +1098,7 @@
|
||||||
@answer.Text
|
@answer.Text
|
||||||
@if (answer.IsOtherOption)
|
@if (answer.IsOtherOption)
|
||||||
{
|
{
|
||||||
<span class="other-option-badge">Other Option</span>
|
|
||||||
}
|
}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue