Add questionnaire status management with draft, archive, and recover options

This commit is contained in:
Qaisyousuf 2025-08-20 13:29:10 +02:00
parent b67eca0729
commit 43461bbb2b
18 changed files with 5925 additions and 964 deletions

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"> /* Premium Glass Morphism Header */
<div class="card justify-content-center p-4 shadow rounded"> .page-header {
<div class="card-body"> background: rgba(255, 255, 255, 0.25);
<h5 class="card-title h5">Send the questionnaire</h5> 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);
}
<div class="row"> .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>

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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