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.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Model
{
@ -27,6 +21,9 @@ namespace Model
[ForeignKey("QuestionnaireId")]
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
{
public class Questionnaire
{
public Questionnaire()
{
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 string? Title { get; set; }
public string? Description { 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;
}
// EXISTING METHOD - Keep exactly as is
public void Add(Questionnaire questionnaire)
{
_context.Questionnaires.Add(questionnaire);
}
// EXISTING METHOD - Keep exactly as is
public async Task commitAsync()
{
await _context.SaveChangesAsync();
}
//public void Delete(int? id)
//{
// var questionnairId = GetQuesById(id);
// _context.Questionnaires.Remove(questionnairId);
//}
// EXISTING METHOD - Keep exactly as is
public List<Questionnaire> GetAllQuestions()
{
return _context.Questionnaires.ToList();
}
// EXISTING METHOD - Keep exactly as is
public Questionnaire GetQuesById(int? id)
{
return _context.Questionnaires.Find(id);
}
// UPDATE THIS METHOD - Add filter for active questions only
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)
{
return _context.Questionnaires // ✅ No AsNoTracking for edit operations!
.Include(x => x.Questions)
return _context.Questionnaires
.Include(x => x.Questions.Where(q => q.IsActive)) // Only get active questions
.ThenInclude(x => x.Answers)
.FirstOrDefault(x => x.Id == id);
}
// EXISTING METHOD - Keep exactly as is
public async Task Update(Questionnaire questionnaire)
{
_context.Questionnaires.Update(questionnaire);
await _context.SaveChangesAsync();
await _context.SaveChangesAsync();
}
// EXISTING METHOD - Keep exactly as is
public async Task Delete(int? id)
{
if (id == null)
{
throw new ArgumentNullException(nameof(id), "ID cannot be null");
}
var questionnaire = GetQuesById(id);
if (questionnaire == null)
{
throw new ArgumentException("Questionnaire not found", nameof(id));
}
_context.Questionnaires.Remove(questionnaire);
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.Collections.Generic;
using System.Linq;
@ -19,5 +20,13 @@ namespace Services.Interaces
Task Delete(int? id);
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;
_emailServices = emailServices;
}
public IActionResult Index()
public async Task<IActionResult> Index()
{
var questionnaire = _questionnaire.GetQuestionnairesWithQuestion();
var question = _question.GetQuestionsWithAnswers();
var questionnaire = _questionnaire.GetAllQuestionnairesWithStatus(); // Use new method
var question = _question.GetQuestionsWithAnswers(); // Keep your existing line
List<QuestionnaireViewModel> viewmodel = new List<QuestionnaireViewModel>();
foreach (var item in questionnaire)
{
// Check if this questionnaire has responses
var hasResponses = await _questionnaire.HasResponses(item.Id);
viewmodel.Add(new QuestionnaireViewModel
{
// EXISTING MAPPING (keep exactly as-is):
Id = item.Id,
Description = item.Description,
Title = item.Title,
Questions = item.Questions,
// ADD NEW STATUS MAPPING:
Status = item.Status,
CreatedDate = item.CreatedDate,
PublishedDate = item.PublishedDate,
ArchivedDate = item.ArchivedDate,
HasResponses = hasResponses
});
}
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]
public IActionResult Create()
@ -143,27 +172,28 @@ namespace Web.Areas.Admin.Controllers
}
[HttpGet]
public IActionResult Edit(int? id)
public IActionResult Edit(int id)
{
var questionTypes = Enum.GetValues(typeof(QuestionType))
.Cast<QuestionType>()
.Select(e => new SelectListItem { Value = e.ToString(), Text = e.ToString() });
.Cast<QuestionType>()
.Select(e => new SelectListItem { Value = e.ToString(), Text = e.ToString() });
ViewBag.QuestionTypes = questionTypes;
var questionnaire = _questionnaire.GetQuestionnaireWithQuestionAndAnswer(id);
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
{
Id = questionnaire.Id,
Title = questionnaire.Title,
Description = questionnaire.Description,
Questions = questionnaire.Questions
.Select(q => new Question
{
@ -171,18 +201,13 @@ namespace Web.Areas.Admin.Controllers
Text = q.Text,
Type = q.Type,
QuestionnaireId = q.QuestionnaireId,
Answers = q.Answers.Select(a => new Answer
{
Id = a.Id,
Text = a.Text,
Question = a.Question,
QuestionId = a.QuestionId
QuestionId = a.QuestionId,
IsOtherOption = a.IsOtherOption
}).ToList()
}).ToList()
};
@ -193,9 +218,13 @@ namespace Web.Areas.Admin.Controllers
[HttpPost]
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))
.Cast<QuestionType>()
.Select(e => new SelectListItem { Value = e.ToString(), Text = e.ToString() });
.Cast<QuestionType>()
.Select(e => new SelectListItem { Value = e.ToString(), Text = e.ToString() });
ViewBag.QuestionTypes = questionTypes;
if (ModelState.IsValid)
@ -203,10 +232,11 @@ namespace Web.Areas.Admin.Controllers
try
{
using var transaction = await _context.Database.BeginTransactionAsync();
Console.WriteLine("Database transaction started");
try
{
// Step 1: Update the questionnaire basic info
// Step 1: Get the questionnaire with its current status
var existingQuestionnaire = await _context.Questionnaires
.FirstOrDefaultAsync(q => q.Id == viewModel.Id);
@ -215,115 +245,270 @@ namespace Web.Areas.Admin.Controllers
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.Description = viewModel.Description;
// Step 2: Get all existing questions for this questionnaire
// Step 4: Get existing 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();
// Step 3: Delete ALL answers first (foreign key constraint)
if (existingQuestions.Any())
Console.WriteLine($"Found {existingQuestions.Count} existing active questions");
// Step 5: Handle editing based on questionnaire status
switch (existingQuestionnaire.Status)
{
var questionIds = existingQuestions.Select(q => q.Id).ToList();
var existingAnswers = await _context.Answers
.Where(a => questionIds.Contains(a.QuestionId))
.ToListAsync();
case QuestionnaireStatus.Draft:
Console.WriteLine("DRAFT MODE: Full editing allowed");
await HandleDraftQuestionnaire(viewModel, existingQuestions, existingQuestionnaire.Id);
break;
_context.Answers.RemoveRange(existingAnswers);
await _context.SaveChangesAsync();
}
// Step 4: Delete ALL questions
_context.Questions.RemoveRange(existingQuestions);
await _context.SaveChangesAsync();
// Step 5: Add new questions (only if provided and valid)
int newQuestionsAdded = 0;
if (viewModel.Questions != null && viewModel.Questions.Count > 0)
{
var validQuestions = viewModel.Questions
.Where(q => !string.IsNullOrWhiteSpace(q.Text))
.ToList();
foreach (var questionViewModel in validQuestions)
{
var newQuestion = new Question
{
Text = questionViewModel.Text.Trim(),
Type = questionViewModel.Type,
QuestionnaireId = viewModel.Id
};
_context.Questions.Add(newQuestion);
await _context.SaveChangesAsync(); // Save to get the ID
// Add answers for this question
if (questionViewModel.Answers != null)
{
var validAnswers = questionViewModel.Answers
.Where(a => !string.IsNullOrWhiteSpace(a.Text))
.ToList();
foreach (var answerViewModel in validAnswers)
{
var newAnswer = new Answer
{
Text = answerViewModel.Text.Trim(),
IsOtherOption = answerViewModel.IsOtherOption,
QuestionId = newQuestion.Id
};
_context.Answers.Add(newAnswer);
}
if (validAnswers.Any())
{
await _context.SaveChangesAsync();
}
}
newQuestionsAdded++;
}
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: Get final count for success message
// Step 7: Success message
var finalQuestionCount = await _context.Questions
.Where(q => q.QuestionnaireId == viewModel.Id)
.CountAsync();
.CountAsync(q => q.QuestionnaireId == viewModel.Id && q.IsActive);
// Success message
if (finalQuestionCount == 0)
TempData["Success"] = $"Questionnaire updated successfully with {finalQuestionCount} question(s)!";
if (existingQuestionnaire.Status == QuestionnaireStatus.Published)
{
TempData["Success"] = "Questionnaire updated successfully. All questions have been removed.";
}
else
{
TempData["Success"] = $"Questionnaire updated successfully with {finalQuestionCount} question(s).";
TempData["Info"] = "Limited editing was applied to preserve response data integrity.";
}
return RedirectToAction(nameof(Index));
}
catch (Exception)
catch (Exception ex)
{
Console.WriteLine($"ERROR in transaction: {ex.Message}");
await transaction.RollbackAsync();
throw;
}
}
catch (Exception ex)
{
ModelState.AddModelError("", "An error occurred while updating the questionnaire. Please try again.");
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();
_context.Answers.RemoveRange(answersToDelete);
await _context.SaveChangesAsync();
// Delete questions
_context.Questions.RemoveRange(questionsToDelete);
await _context.SaveChangesAsync();
}
// Process remaining questions (full editing allowed)
await ProcessQuestions(viewModel, existingQuestions, questionnaireId, allowFullEditing: true);
}
// Handle Published Questionnaires (Limited Editing)
private async Task HandlePublishedQuestionnaire(EditQuestionnaireViewModel viewModel,
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
.Where(q => !string.IsNullOrWhiteSpace(q.Text))
.ToList();
Console.WriteLine($"Processing {validQuestions.Count} valid questions");
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
{
Text = questionViewModel.Text.Trim(),
Type = questionViewModel.Type,
QuestionnaireId = questionnaireId,
IsActive = true,
CreatedDate = DateTime.UtcNow
};
_context.Questions.Add(newQuestion);
await _context.SaveChangesAsync(); // Save to get the ID
Console.WriteLine($"New question created with ID: {newQuestion.Id}");
// 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
.Where(a => !string.IsNullOrWhiteSpace(a.Text))
.ToList();
Console.WriteLine($"Adding {validAnswers.Count} answers to question {question.Id}");
foreach (var answerViewModel in validAnswers)
{
var newAnswer = new Answer
{
Text = answerViewModel.Text.Trim(),
IsOtherOption = answerViewModel.IsOtherOption,
QuestionId = question.Id
};
_context.Answers.Add(newAnswer);
}
if (validAnswers.Any())
{
await _context.SaveChangesAsync();
}
}
}
[HttpGet]
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
public class SaveAnswerConditionRequest
{

View file

@ -9,106 +9,270 @@ using Web.ViewModel.QuestionnaireVM;
namespace Web.Areas.Admin.Controllers
{
public class UserResponseController : Controller
{
private readonly SurveyContext _context;
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;
_userResponse = userResponse;
_logger = logger;
}
public async Task<IActionResult> Index()
{
var responses = await GetAllResponsesWithDetailsAsync(); // Fetch the data
return View(responses); // Pass the data to the view
try
{
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()
{
return await _context.Responses
.Include(r => r.Questionnaire) // Ensure the Questionnaire data is included
.OrderBy(r => r.Id) // Optional: Order by submission date
.Include(r => r.Questionnaire)
.OrderByDescending(r => r.SubmissionDate) // Most recent first
.ToListAsync();
}
[HttpGet]
public async Task<IActionResult> ViewResponse(int id) // Pass the response ID
public async Task<IActionResult> ViewResponse(int id)
{
var response = await _context.Responses
try
{
var response = await _context.Responses
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.Question)
.ThenInclude(q => q.Answers) // Load all possible answers for the questions
.ThenInclude(q => q.Answers)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers) // Load the answers selected by the user
.ThenInclude(rd => rd.ResponseAnswers)
.Include(r => r.Questionnaire)
.AsNoTracking()
.FirstOrDefaultAsync(r => r.Id == id);
if (response == null)
{
return NotFound(); // If no response is found, return a NotFound result
if (response == null)
{
TempData["Error"] = "Response not found.";
return RedirectToAction(nameof(Index));
}
return View(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving response {ResponseId}", id);
TempData["Error"] = "Error loading response details.";
return RedirectToAction(nameof(Index));
}
return View(response); // Pass the response to the view
}
public async Task<IActionResult> UserResponsesStatus(string userName)
{
var responses = await _userResponse.GetResponsesByUserAsync(userName);
if (responses == null || !responses.Any())
try
{
return NotFound();
var responses = await _userResponse.GetResponsesByUserAsync(userName);
if (responses == null || !responses.Any())
{
TempData["Warning"] = "No responses found for this user.";
return RedirectToAction(nameof(Index));
}
var userEmail = responses.First().UserEmail;
var viewModel = new UserResponsesViewModel
{
UserName = userName,
UserEmail = userEmail,
Responses = responses.ToList()
};
return View(viewModel);
}
var userEmail = responses.First().UserEmail;
var viewModel = new UserResponsesViewModel
catch (Exception ex)
{
UserName = userName,
UserEmail = userEmail,
Responses = responses.ToList()
};
return View(viewModel);
_logger.LogError(ex, "Error retrieving user responses for {UserName}", userName);
TempData["Error"] = "Error loading user responses.";
return RedirectToAction(nameof(Index));
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
var response = await _context.Responses.FindAsync(id);
if (response == null)
try
{
return NotFound();
var response = await _context.Responses
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.FirstOrDefaultAsync(r => r.Id == id);
if (response == null)
{
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);
await _context.SaveChangesAsync();
_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.";
}
_context.Responses.Remove(response);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
[HttpPost]
[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())
{
TempData["Warning"] = "No responses selected for deletion.";
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.";
}
_context.Responses.RemoveRange(responses);
await _context.SaveChangesAsync();
TempData["Success"] = "User response deleted successfully";
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,77 +14,198 @@ namespace Web.Areas.Admin.Controllers
{
private readonly SurveyContext _context;
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;
_userResponse = userResponse;
_logger = logger;
}
public async Task<IActionResult> Index()
{
var usersWithQuestionnaires = await _context.Responses
.Include(r => r.Questionnaire)
.GroupBy(r => r.UserEmail)
.Select(g => new UserResponsesViewModel
try
{
UserName = g.FirstOrDefault().UserName, // Display the first username found for the email
UserEmail = g.Key,
Responses = g.Select(r => new Response
{
Questionnaire = r.Questionnaire
}).Distinct().ToList()
})
.ToListAsync();
var usersWithQuestionnaires = await _context.Responses
.Include(r => r.Questionnaire)
.GroupBy(r => r.UserEmail)
.Select(g => new UserResponsesViewModel
{
UserName = g.FirstOrDefault().UserName,
UserEmail = g.Key,
Responses = g.Select(r => new Response
{
Questionnaire = r.Questionnaire
}).Distinct().ToList()
})
.ToListAsync();
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)
{
var responses = await _context.Responses
.Include(r => r.Questionnaire)
.ThenInclude(q => q.Questions.OrderBy(qu => qu.Id))
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.Question)
.ThenInclude(q => q.Answers)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.Where(r => r.UserEmail == userEmail)
.ToListAsync();
if (responses == null || !responses.Any())
try
{
return NotFound();
var responses = await _context.Responses
.Include(r => r.Questionnaire)
.ThenInclude(q => q.Questions.OrderBy(qu => qu.Id))
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.Question)
.ThenInclude(q => q.Answers)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.Where(r => r.UserEmail == userEmail)
.ToListAsync();
if (responses == null || !responses.Any())
{
TempData["Warning"] = "No responses found for this user.";
return RedirectToAction(nameof(Index));
}
var userName = responses.First().UserName;
var viewModel = new UserResponsesViewModel
{
UserName = userName,
UserEmail = userEmail,
Responses = responses
};
return View(viewModel);
}
var userName = responses.First().UserName;
var viewModel = new UserResponsesViewModel
catch (Exception ex)
{
UserName = userName,
UserEmail = userEmail,
Responses = responses
};
return View(viewModel);
_logger.LogError(ex, "Error retrieving user responses for {UserEmail}", userEmail);
TempData["Error"] = "Error loading user response details.";
return RedirectToAction(nameof(Index));
}
}
[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));
}
var responsesToDelete = await _context.Responses
.Where(r => selectedEmails.Contains(r.UserEmail))
.ToListAsync();
if (responsesToDelete.Any())
try
{
_logger.LogInformation("Attempting to delete responses for {Count} users: {Emails}",
selectedEmails.Count, string.Join(", ", selectedEmails));
var responsesToDelete = await _context.Responses
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.Where(r => selectedEmails.Contains(r.UserEmail))
.ToListAsync();
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);
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));
@ -92,29 +213,40 @@ namespace Web.Areas.Admin.Controllers
public async Task<IActionResult> GenerateReport(string userEmail, string format)
{
var responses = await _context.Responses
.Include(r => r.Questionnaire)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.Question)
.ThenInclude(q => q.Answers)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.Where(r => r.UserEmail == userEmail)
.ToListAsync();
if (responses == null || !responses.Any())
try
{
return NotFound();
var responses = await _context.Responses
.Include(r => r.Questionnaire)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.Question)
.ThenInclude(q => q.Answers)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.Where(r => r.UserEmail == userEmail)
.ToListAsync();
if (responses == null || !responses.Any())
{
TempData["Warning"] = "No responses found for this user.";
return RedirectToAction(nameof(Index));
}
switch (format.ToLower())
{
case "pdf":
return GeneratePdfReport(responses);
case "excel":
return GenerateExcelReport(responses);
default:
TempData["Error"] = "Unsupported report format.";
return RedirectToAction(nameof(Index));
}
}
switch (format.ToLower())
catch (Exception ex)
{
case "pdf":
return GeneratePdfReport(responses);
case "excel":
return GenerateExcelReport(responses);
default:
return BadRequest("Unsupported report format.");
_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 document = new Document(PageSize.A4, 50, 50, 25, 25);
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();
@ -190,7 +322,7 @@ namespace Web.Areas.Admin.Controllers
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));
// NEW: Include "Other" text if available
// Include "Other" text if available
if (!string.IsNullOrEmpty(detail.OtherText))
{
answers += string.IsNullOrEmpty(answers)
@ -228,7 +360,7 @@ namespace Web.Areas.Admin.Controllers
var logo = new FileInfo(logoPath);
var picture = worksheet.Drawings.AddPicture("Logo", logo);
picture.SetPosition(0, 0, 0, 0);
picture.SetSize(300, 70); // Adjust the size as needed
picture.SetSize(300, 70);
}
// 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));
// NEW: Include "Other" text if available
// Include "Other" text if available
if (!string.IsNullOrEmpty(detail.OtherText))
{
answers += string.IsNullOrEmpty(answers)
@ -299,21 +431,31 @@ namespace Web.Areas.Admin.Controllers
public async Task<IActionResult> GenerateQuestionnairePdfReport(int questionnaireId)
{
var response = await _context.Responses
.Include(r => r.Questionnaire)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.Question)
.ThenInclude(q => q.Answers)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.FirstOrDefaultAsync(r => r.QuestionnaireId == questionnaireId);
if (response == null)
try
{
return NotFound();
}
var response = await _context.Responses
.Include(r => r.Questionnaire)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.Question)
.ThenInclude(q => q.Answers)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.FirstOrDefaultAsync(r => r.QuestionnaireId == questionnaireId);
return GeneratePdfReportForQuestionnaire(response);
if (response == null)
{
TempData["Warning"] = "No response found for this questionnaire.";
return RedirectToAction(nameof(Index));
}
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)
@ -324,7 +466,7 @@ namespace Web.Areas.Admin.Controllers
var stream = new MemoryStream();
var document = new Document(PageSize.A4, 50, 50, 25, 25);
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();
@ -394,7 +536,7 @@ namespace Web.Areas.Admin.Controllers
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));
// NEW: Include "Other" text if available
// Include "Other" text if available
if (!string.IsNullOrEmpty(detail.OtherText))
{
answers += string.IsNullOrEmpty(answers)
@ -416,21 +558,31 @@ namespace Web.Areas.Admin.Controllers
public async Task<IActionResult> GenerateQuestionnaireExcelReport(int questionnaireId)
{
var response = await _context.Responses
.Include(r => r.Questionnaire)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.Question)
.ThenInclude(q => q.Answers)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.FirstOrDefaultAsync(r => r.QuestionnaireId == questionnaireId);
if (response == null)
try
{
return NotFound();
}
var response = await _context.Responses
.Include(r => r.Questionnaire)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.Question)
.ThenInclude(q => q.Answers)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.FirstOrDefaultAsync(r => r.QuestionnaireId == questionnaireId);
return GenerateExcelReportForQuestionnaire(response);
if (response == null)
{
TempData["Warning"] = "No response found for this questionnaire.";
return RedirectToAction(nameof(Index));
}
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)
@ -449,7 +601,7 @@ namespace Web.Areas.Admin.Controllers
var logo = new FileInfo(logoPath);
var picture = worksheet.Drawings.AddPicture("Logo", logo);
picture.SetPosition(0, 0, 2, 0);
picture.SetSize(300, 60); // Adjust the size as needed
picture.SetSize(300, 60);
}
// 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));
// NEW: Include "Other" text if available
// Include "Other" text if available
if (!string.IsNullOrEmpty(detail.OtherText))
{
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");
}
}
// 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
@{
ViewData["Title"] = "Send";
ViewData["Title"] = "Send Questionnaire";
}
<style>
/* Modern Design System with Advanced Visual Effects */
@@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap');
:root {
--primary-color: #6366f1;
--primary-light: #8b5cf6;
--primary-dark: #4338ca;
--primary-ultra: #3730a3;
--success-color: #10b981;
--success-light: #34d399;
--success-dark: #059669;
--info-color: #06b6d4;
--info-light: #22d3ee;
--info-dark: #0891b2;
--warning-color: #f59e0b;
--warning-light: #fbbf24;
--warning-dark: #d97706;
--danger-color: #ef4444;
--danger-light: #fca5a5;
--danger-dark: #dc2626;
--gray-50: #f8fafc;
--gray-100: #f1f5f9;
--gray-200: #e2e8f0;
--gray-300: #cbd5e1;
--gray-400: #94a3b8;
--gray-500: #64748b;
--gray-600: #475569;
--gray-700: #334155;
--gray-800: #1e293b;
--gray-900: #0f172a;
/* Advanced Shadows */
--shadow-glow: 0 0 40px rgba(99, 102, 241, 0.15);
--shadow-ultra: 0 32px 64px -12px rgba(0, 0, 0, 0.25);
--shadow-floating: 0 20px 40px -8px rgba(99, 102, 241, 0.2);
--shadow-premium: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 80px rgba(99, 102, 241, 0.1);
/* Border Radius */
--border-radius-sm: 12px;
--border-radius-md: 16px;
--border-radius-lg: 24px;
--border-radius-xl: 32px;
--border-radius-2xl: 40px;
/* Animations */
--ease-premium: cubic-bezier(0.23, 1, 0.32, 1);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
* {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
min-height: 100vh;
margin: 0;
padding: 0;
position: relative;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 3rem 1rem;
position: relative;
z-index: 1;
}
<div class="container mt-4">
<div class="card justify-content-center p-4 shadow rounded">
<div class="card-body">
<h5 class="card-title h5">Send the questionnaire</h5>
<div class="row">
<form asp-action="SendQuestionnaire">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="QuestionnaireId" class="control-label">Questionnaire</label> <!-- Display ViewBag data in the label -->
<input type="text" class="form-control" value="@ViewBag.questionnaireName" disabled /> <!-- Display ViewBag data in the disabled textbox -->
<span asp-validation-for="QuestionnaireId" class="text-danger"></span>
/* Premium Glass Morphism Header */
.page-header {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: var(--border-radius-2xl);
box-shadow: var(--shadow-premium);
padding: 3rem;
margin-bottom: 3rem;
position: relative;
overflow: hidden;
animation: slideDown 0.8s var(--ease-premium);
}
</div>
<div class="form-group">
<label asp-for="Emails" class="control-label">Email Addresses (separate with commas)</label>
<textarea asp-for="Emails" class="form-control"></textarea>
<span asp-validation-for="Emails" class="text-danger"></span>
</div>
.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;
}
<div class="form-group">
<label asp-for="ExpirationDateTime" class="control-label"></label>
<input asp-for="ExpirationDateTime" class="form-control"/>
<span asp-validation-for="ExpirationDateTime" class="text-danger"></span>
</div>
<div class="form-group">
<input type="hidden" asp-for="QuestionnaireId" /> <!-- Use hidden input for QuestionnaireId -->
</div>
<div class="form-group">
<input type="submit" value="Send" class="btn btn-primary" />
</div>
</form>
@@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">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<!-- Questionnaire Selection -->
<div class="form-group">
<label asp-for="QuestionnaireId" class="control-label">
<i class="bi bi-clipboard-data-fill"></i>
Selected Questionnaire
</label>
<input type="text" class="form-control" value="@ViewBag.questionnaireName" disabled />
</div>
<!-- Email Addresses -->
<div class="form-group">
<label asp-for="Emails" class="control-label">
<i class="bi bi-people-fill"></i>
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>
<!-- Expiration Date -->
<div class="form-group">
<label asp-for="ExpirationDateTime" class="control-label">
<i class="bi bi-calendar-event-fill"></i>
Survey Expiration
</label>
<input asp-for="ExpirationDateTime"
class="form-control datetime-input"
type="datetime-local"
min="@DateTime.Now.ToString("yyyy-MM-ddTHH:mm")" />
</div>
<!-- Hidden Fields -->
<input type="hidden" asp-for="QuestionnaireId" />
<!-- 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>
</form>
</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>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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"));
b.Property<DateTime>("CreatedDate")
.HasColumnType("datetime2");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<int>("QuestionnaireId")
.HasColumnType("int");
@ -458,9 +464,21 @@ namespace Web.Migrations
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)");

View file

@ -24,7 +24,15 @@ namespace Web.ViewModel.QuestionnaireVM
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-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 {
margin-top: 12px;
@ -1078,7 +1061,7 @@
@answer.Text
@if (answer.IsOtherOption)
{
<span class="other-option-badge">Other Option</span>
}
</label>
@ -1115,7 +1098,7 @@
@answer.Text
@if (answer.IsOtherOption)
{
<span class="other-option-badge">Other Option</span>
}
</label>