Add questionnaire status management with draft, archive, and recover options
This commit is contained in:
parent
b67eca0729
commit
43461bbb2b
18 changed files with 5925 additions and 964 deletions
|
|
@ -1,11 +1,5 @@
|
|||
using System;
|
||||
using System.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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
Model/QuestionnaireStatus.cs
Normal file
15
Model/QuestionnaireStatus.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Model
|
||||
{
|
||||
public enum QuestionnaireStatus
|
||||
{
|
||||
Draft = 0, // Can be fully edited/deleted
|
||||
Published = 1, // Live, accepting responses - limited editing
|
||||
Archived = 2 // Completed, read-only
|
||||
}
|
||||
}
|
||||
|
|
@ -13,72 +13,129 @@ namespace Services.Implemnetation
|
|||
{
|
||||
_context = Context;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
574
Web/Areas/Admin/Views/Questionnaire/StatusGuide.cshtml
Normal file
574
Web/Areas/Admin/Views/Questionnaire/StatusGuide.cshtml
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
@{
|
||||
ViewData["Title"] = "Questionnaire Status Guide";
|
||||
|
||||
}
|
||||
|
||||
<style>
|
||||
.status-guide-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.guide-header {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.guide-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.guide-header p {
|
||||
margin: 0.5rem 0 0;
|
||||
opacity: 0.9;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
padding: 2rem;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-card:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.status-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.draft-card::before {
|
||||
background: linear-gradient(90deg, #6b7280, #9ca3af);
|
||||
}
|
||||
|
||||
.published-card::before {
|
||||
background: linear-gradient(90deg, #10b981, #34d399);
|
||||
}
|
||||
|
||||
.archived-card::before {
|
||||
background: linear-gradient(90deg, #1f2937, #374151);
|
||||
}
|
||||
|
||||
.status-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.draft-icon {
|
||||
background: linear-gradient(135deg, #6b7280, #9ca3af);
|
||||
}
|
||||
|
||||
.published-icon {
|
||||
background: linear-gradient(135deg, #10b981, #34d399);
|
||||
}
|
||||
|
||||
.archived-icon {
|
||||
background: linear-gradient(135deg, #1f2937, #374151);
|
||||
}
|
||||
|
||||
.status-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-subtitle {
|
||||
color: #6b7280;
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.capabilities-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.capability-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.capability-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.capability-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.capability-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.can-do {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cannot-do {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.limited {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.capability-text {
|
||||
font-size: 0.9rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.workflow-section {
|
||||
background: #f8fafc;
|
||||
padding: 2rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.workflow-title {
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.workflow-steps {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.workflow-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.workflow-arrow {
|
||||
font-size: 2rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.75rem;
|
||||
color: white;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.tips-section {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.tips-title {
|
||||
color: #0369a1;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
color: #0c4a6e;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@@media (max-width: 768px) {
|
||||
.status-grid
|
||||
|
||||
{
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.status-card:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.workflow-steps {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.workflow-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Back Button -->
|
||||
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="status-guide-container">
|
||||
<div class="guide-header">
|
||||
<h1><i class="bi bi-clipboard-check"></i> Questionnaire Status Guide</h1>
|
||||
<p>Complete breakdown of what you can do in each status</p>
|
||||
</div>
|
||||
|
||||
<div class="status-grid">
|
||||
<!-- DRAFT STATUS -->
|
||||
<div class="status-card draft-card">
|
||||
<div class="status-header">
|
||||
<div class="status-icon draft-icon"><i class="bi bi-pencil"></i></div>
|
||||
<div>
|
||||
<h2 class="status-title">Draft</h2>
|
||||
<p class="status-subtitle">Full editing freedom</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="capabilities-section">
|
||||
<h3 class="section-title"><i class="bi bi-check-circle text-success"></i> What You CAN Do</h3>
|
||||
<ul class="capability-list">
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Edit questionnaire title and description</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Add new questions</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Edit existing questions (text, type)</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Delete questions completely</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Add/edit/delete answers</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Reorder questions</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Delete entire questionnaire</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Publish questionnaire</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="capabilities-section">
|
||||
<h3 class="section-title"><i class="bi bi-x-circle text-danger"></i> What You CANNOT Do</h3>
|
||||
<ul class="capability-list">
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon cannot-do">✗</div>
|
||||
<span class="capability-text">Send to respondents (must publish first)</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon cannot-do">✗</div>
|
||||
<span class="capability-text">Collect responses (not live yet)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tips-section">
|
||||
<h4 class="tips-title"><i class="bi bi-lightbulb"></i> Best Practices</h4>
|
||||
<div class="tip-item">• Perfect your questions before publishing</div>
|
||||
<div class="tip-item">• Test question flow and logic</div>
|
||||
<div class="tip-item">• Review with team members</div>
|
||||
<div class="tip-item">• Preview how it looks to respondents</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PUBLISHED STATUS -->
|
||||
<div class="status-card published-card">
|
||||
<div class="status-header">
|
||||
<div class="status-icon published-icon"><i class="bi bi-broadcast"></i></div>
|
||||
<div>
|
||||
<h2 class="status-title">Published</h2>
|
||||
<p class="status-subtitle">Live & accepting responses</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="capabilities-section">
|
||||
<h3 class="section-title"><i class="bi bi-check-circle text-success"></i> What You CAN Do</h3>
|
||||
<ul class="capability-list">
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Edit questionnaire title and description</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Edit question text (preserve meaning)</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Add new questions</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Send questionnaire to respondents</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">View responses and analytics</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Archive questionnaire</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="capabilities-section">
|
||||
<h3 class="section-title"><i class="bi bi-exclamation-triangle text-warning"></i> Limited Actions</h3>
|
||||
<ul class="capability-list">
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon limited">⚠</div>
|
||||
<span class="capability-text">Remove questions (hidden, not deleted)</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon limited">⚠</div>
|
||||
<span class="capability-text">Edit answers (only for questions without responses)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="capabilities-section">
|
||||
<h3 class="section-title"><i class="bi bi-x-circle text-danger"></i> What You CANNOT Do</h3>
|
||||
<ul class="capability-list">
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon cannot-do">✗</div>
|
||||
<span class="capability-text">Delete questionnaire (has responses)</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon cannot-do">✗</div>
|
||||
<span class="capability-text">Permanently delete questions with responses</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon cannot-do">✗</div>
|
||||
<span class="capability-text">Change answers for questions with responses</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon cannot-do">✗</div>
|
||||
<span class="capability-text">Change question types that affect existing responses</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tips-section">
|
||||
<h4 class="tips-title"><i class="bi bi-lightbulb"></i> Best Practices</h4>
|
||||
<div class="tip-item">• Make only essential text changes</div>
|
||||
<div class="tip-item">• Add new questions at the end</div>
|
||||
<div class="tip-item">• Monitor response data regularly</div>
|
||||
<div class="tip-item">• Consider creating a new version for major changes</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ARCHIVED STATUS -->
|
||||
<div class="status-card archived-card">
|
||||
<div class="status-header">
|
||||
<div class="status-icon archived-icon"><i class="bi bi-archive"></i></div>
|
||||
<div>
|
||||
<h2 class="status-title">Archived</h2>
|
||||
<p class="status-subtitle">Read-only & complete</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="capabilities-section">
|
||||
<h3 class="section-title"><i class="bi bi-check-circle text-success"></i> What You CAN Do</h3>
|
||||
<ul class="capability-list">
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">View questionnaire details</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">View all responses and data</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Export response data</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Generate reports and analytics</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon can-do">✓</div>
|
||||
<span class="capability-text">Duplicate questionnaire as new draft</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="capabilities-section">
|
||||
<h3 class="section-title"><i class="bi bi-exclamation-triangle text-warning"></i> Limited Actions</h3>
|
||||
<ul class="capability-list">
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon limited">⚠</div>
|
||||
<span class="capability-text">Revert to draft (only if no responses)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="capabilities-section">
|
||||
<h3 class="section-title"><i class="bi bi-x-circle text-danger"></i> What You CANNOT Do</h3>
|
||||
<ul class="capability-list">
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon cannot-do">✗</div>
|
||||
<span class="capability-text">Edit any questionnaire content</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon cannot-do">✗</div>
|
||||
<span class="capability-text">Add or remove questions</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon cannot-do">✗</div>
|
||||
<span class="capability-text">Send to new respondents</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon cannot-do">✗</div>
|
||||
<span class="capability-text">Collect new responses</span>
|
||||
</li>
|
||||
<li class="capability-item">
|
||||
<div class="capability-icon cannot-do">✗</div>
|
||||
<span class="capability-text">Delete questionnaire (permanent archive)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tips-section">
|
||||
<h4 class="tips-title"><i class="bi bi-lightbulb"></i> Best Practices</h4>
|
||||
<div class="tip-item">• Export all data before long-term storage</div>
|
||||
<div class="tip-item">• Create final analysis reports</div>
|
||||
<div class="tip-item">• Document key insights and findings</div>
|
||||
<div class="tip-item">• Use as template for future questionnaires</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workflow-section">
|
||||
<h2 class="workflow-title"><i class="bi bi-arrow-repeat"></i> Status Workflow</h2>
|
||||
<div class="workflow-steps">
|
||||
<div class="workflow-step">
|
||||
<div class="step-icon draft-icon"><i class="bi bi-pencil"></i></div>
|
||||
<div class="step-title">Draft</div>
|
||||
<div class="step-description">Create & perfect your questionnaire</div>
|
||||
</div>
|
||||
<div class="workflow-arrow">→</div>
|
||||
<div class="workflow-step">
|
||||
<div class="step-icon published-icon"><i class="bi bi-broadcast"></i></div>
|
||||
<div class="step-title">Published</div>
|
||||
<div class="step-description">Live & collecting responses</div>
|
||||
</div>
|
||||
<div class="workflow-arrow">→</div>
|
||||
<div class="workflow-step">
|
||||
<div class="step-icon archived-icon"><i class="bi bi-archive"></i></div>
|
||||
<div class="step-title">Archived</div>
|
||||
<div class="step-description">Complete & preserved</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<div class="alert alert-info d-inline-block">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Key Benefit:</strong> This status system completely eliminates foreign key constraint errors while providing professional survey management!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
865
Web/Migrations/20250816123754_AddQuestionnaireStatusAndSoftDelete.Designer.cs
generated
Normal file
865
Web/Migrations/20250816123754_AddQuestionnaireStatusAndSoftDelete.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,865 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Web.Migrations
|
||||
{
|
||||
[DbContext(typeof(SurveyContext))]
|
||||
[Migration("20250816123754_AddQuestionnaireStatusAndSoftDelete")]
|
||||
partial class AddQuestionnaireStatusAndSoftDelete
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Address", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("CVR")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("City")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Mobile")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PostalCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("State")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Street")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Addresss");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Answer", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ConditionJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsOtherOption")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("QuestionId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("QuestionId");
|
||||
|
||||
b.ToTable("Answers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Banner", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Banners");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Footer", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ImageUlr")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("LastUpdated")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Sitecopyright")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UpdatedBy")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Footers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.FooterSocialMedia", b =>
|
||||
{
|
||||
b.Property<int>("FooterId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SocialId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("FooterId", "SocialId");
|
||||
|
||||
b.HasIndex("SocialId");
|
||||
|
||||
b.ToTable("FooterSocialMedias");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Page", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BannerId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("FooterId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BannerId");
|
||||
|
||||
b.HasIndex("FooterId");
|
||||
|
||||
b.ToTable("Pages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Question", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("QuestionnaireId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("QuestionnaireId");
|
||||
|
||||
b.ToTable("Questions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Questionnaire", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime?>("ArchivedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("CreatedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("PublishedDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Questionnaires");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Response", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("QuestionnaireId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("SubmissionDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("UserEmail")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("QuestionnaireId");
|
||||
|
||||
b.ToTable("Responses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.ResponseAnswer", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AnswerId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ResponseDetailId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ResponseDetailId");
|
||||
|
||||
b.ToTable("ResponseAnswers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.ResponseDetail", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("OtherText")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("QuestionId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("QuestionType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ResponseId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SkipReason")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TextResponse")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("QuestionId");
|
||||
|
||||
b.HasIndex("ResponseId");
|
||||
|
||||
b.ToTable("ResponseDetails");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.SentNewsletterEamil", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Body")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Geo")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsBlocked")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsBounced")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsClicked")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDelivered")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsOpened")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsSent")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsSpam")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsUnsubscribed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime>("ReceivedActivity")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("RecipientEmail")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("SentDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SentNewsletterEamils");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.SocialMedia", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SocialMedia");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Subscription", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsSubscribed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Subscriptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Model.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Model.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Model.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Model.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Answer", b =>
|
||||
{
|
||||
b.HasOne("Model.Question", "Question")
|
||||
.WithMany("Answers")
|
||||
.HasForeignKey("QuestionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Question");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.FooterSocialMedia", b =>
|
||||
{
|
||||
b.HasOne("Model.Footer", "Footer")
|
||||
.WithMany("FooterSocialMedias")
|
||||
.HasForeignKey("FooterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Model.SocialMedia", "SocialMedia")
|
||||
.WithMany("FooterSocialMedias")
|
||||
.HasForeignKey("SocialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Footer");
|
||||
|
||||
b.Navigation("SocialMedia");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Page", b =>
|
||||
{
|
||||
b.HasOne("Model.Banner", "banner")
|
||||
.WithMany()
|
||||
.HasForeignKey("BannerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Model.Footer", "footer")
|
||||
.WithMany()
|
||||
.HasForeignKey("FooterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("banner");
|
||||
|
||||
b.Navigation("footer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Question", b =>
|
||||
{
|
||||
b.HasOne("Model.Questionnaire", "Questionnaire")
|
||||
.WithMany("Questions")
|
||||
.HasForeignKey("QuestionnaireId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Questionnaire");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Response", b =>
|
||||
{
|
||||
b.HasOne("Model.Questionnaire", "Questionnaire")
|
||||
.WithMany()
|
||||
.HasForeignKey("QuestionnaireId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Questionnaire");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.ResponseAnswer", b =>
|
||||
{
|
||||
b.HasOne("Model.ResponseDetail", "ResponseDetail")
|
||||
.WithMany("ResponseAnswers")
|
||||
.HasForeignKey("ResponseDetailId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ResponseDetail");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.ResponseDetail", b =>
|
||||
{
|
||||
b.HasOne("Model.Question", "Question")
|
||||
.WithMany()
|
||||
.HasForeignKey("QuestionId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Model.Response", "Response")
|
||||
.WithMany("ResponseDetails")
|
||||
.HasForeignKey("ResponseId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Question");
|
||||
|
||||
b.Navigation("Response");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Footer", b =>
|
||||
{
|
||||
b.Navigation("FooterSocialMedias");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Question", b =>
|
||||
{
|
||||
b.Navigation("Answers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Questionnaire", b =>
|
||||
{
|
||||
b.Navigation("Questions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.Response", b =>
|
||||
{
|
||||
b.Navigation("ResponseDetails");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.ResponseDetail", b =>
|
||||
{
|
||||
b.Navigation("ResponseAnswers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.SocialMedia", b =>
|
||||
{
|
||||
b.Navigation("FooterSocialMedias");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddQuestionnaireStatusAndSoftDelete : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedDate",
|
||||
table: "Questions",
|
||||
type: "datetime2",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsActive",
|
||||
table: "Questions",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ArchivedDate",
|
||||
table: "Questionnaires",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedDate",
|
||||
table: "Questionnaires",
|
||||
type: "datetime2",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "PublishedDate",
|
||||
table: "Questionnaires",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Status",
|
||||
table: "Questionnaires",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedDate",
|
||||
table: "Questions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsActive",
|
||||
table: "Questions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ArchivedDate",
|
||||
table: "Questionnaires");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedDate",
|
||||
table: "Questionnaires");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PublishedDate",
|
||||
table: "Questionnaires");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Status",
|
||||
table: "Questionnaires");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -434,6 +434,12 @@ namespace Web.Migrations
|
|||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
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)");
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue