From d3a6fd4918ba517d9e18af6a9b97ae500ada6eaf Mon Sep 17 00:00:00 2001 From: Qais Yousuf Date: Tue, 30 Apr 2024 11:30:33 +0200 Subject: [PATCH] Survey Analyzer for each survey created --- Model/QuestionType.cs | 8 +- .../Controllers/QuestionnaireController.cs | 9 +- .../Controllers/SurveyAnalysisController.cs | 115 +++++++++++++ .../Controllers/UserResponseController.cs | 32 ++++ .../Admin/Views/Shared/_AdminLayout.cshtml | 4 + .../Views/SurveyAnalysis/Analysis.cshtml | 133 +++++++++++++++ .../Admin/Views/SurveyAnalysis/Index.cshtml | 64 ++++++++ .../Admin/Views/UserResponse/Index.cshtml | 80 +++++---- .../Views/UserResponse/ViewResponse.cshtml | 7 +- .../QuestionnaireResponseController.cs | 108 ++++++++++++- .../ResponseAnswerViewModel.cs | 2 + .../ResponseQuestionnaireViewModel.cs | 9 +- ...ResponseQuestionnaireWithUsersViewModel.cs | 24 +++ .../QuestionnaireVM/ResponseUserViewModel.cs | 8 + .../SubmittedSurvey.cshtml | 153 ++++++++++++++++++ .../QuestionnaireResponse/ThankYou.cshtml | 8 +- Web/appsettings.json | 2 +- 17 files changed, 700 insertions(+), 66 deletions(-) create mode 100644 Web/Areas/Admin/Controllers/SurveyAnalysisController.cs create mode 100644 Web/Areas/Admin/Views/SurveyAnalysis/Analysis.cshtml create mode 100644 Web/Areas/Admin/Views/SurveyAnalysis/Index.cshtml create mode 100644 Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireWithUsersViewModel.cs create mode 100644 Web/ViewModel/QuestionnaireVM/ResponseUserViewModel.cs create mode 100644 Web/Views/QuestionnaireResponse/SubmittedSurvey.cshtml diff --git a/Model/QuestionType.cs b/Model/QuestionType.cs index dcb5b75..6cabe02 100644 --- a/Model/QuestionType.cs +++ b/Model/QuestionType.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Model +namespace Model { public enum QuestionType { diff --git a/Web/Areas/Admin/Controllers/QuestionnaireController.cs b/Web/Areas/Admin/Controllers/QuestionnaireController.cs index fe0d9f0..4f3d2cf 100644 --- a/Web/Areas/Admin/Controllers/QuestionnaireController.cs +++ b/Web/Areas/Admin/Controllers/QuestionnaireController.cs @@ -13,6 +13,7 @@ using Services.Interaces; using System.Globalization; using System.Security.Cryptography; using System.Text; +using System.Web; using Web.ViewModel.QuestionnaireVM; @@ -470,7 +471,9 @@ namespace Web.Areas.Admin.Controllers var questionnairePath = _configuration["Email:Questionnaire"]; int surveyId = viewModel.QuestionnaireId; - + var userEmailEncoded = HttpUtility.UrlEncode(viewModel.Email); + + DateTime currentDateTime; if (viewModel.ExpirationDateTime.HasValue) { @@ -491,9 +494,9 @@ namespace Web.Areas.Admin.Controllers // Append the expiration date and time to the token (you might want to encrypt it for security) string tokenWithExpiry = $"{token}|{expiryDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ")}"; + var completeUrl = $"{Request.Scheme}://{Request.Host}/{questionnairePath}/{viewModel.QuestionnaireId}?t={tokenWithExpiry}&E={userEmailEncoded}"; - - var completeUrl = $"{Request.Scheme}://{Request.Host}/{questionnairePath}/{viewModel.QuestionnaireId}?t={tokenWithExpiry}"; + //var completeUrl = $"{Request.Scheme}://{Request.Host}/{questionnairePath}/{viewModel.QuestionnaireId}?t={tokenWithExpiry}&E={userEmail}"; //var completeUrl = $"{Request.Scheme}://{Request.Host}/{questionnairePath}/{viewModel.QuestionnaireId}"; diff --git a/Web/Areas/Admin/Controllers/SurveyAnalysisController.cs b/Web/Areas/Admin/Controllers/SurveyAnalysisController.cs new file mode 100644 index 0000000..f3c902a --- /dev/null +++ b/Web/Areas/Admin/Controllers/SurveyAnalysisController.cs @@ -0,0 +1,115 @@ +using Azure; +using Data; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Model; +using Web.ViewModel.QuestionnaireVM; +using Web.ViewModel.QuestionVM; + +namespace Web.Areas.Admin.Controllers +{ + public class SurveyAnalysisController : Controller + { + private readonly SurveyContext _context; + + public SurveyAnalysisController(SurveyContext context) + { + _context = context; + } + public IActionResult Index() + { + var questionnaires = _context.Responses + .Include(r => r.Questionnaire) // Ensure the navigation property is correctly set up in the Response model + .Select(r => r.Questionnaire) + .Distinct() // Ensure each questionnaire is listed once + .ToList(); + + var viewModel = questionnaires.Select(q => new ResponseQuestionnaireWithUsersViewModel + { + Id = q.Id, + Title = q.Title + + }).ToList(); + + return View(viewModel); + } + + + [HttpGet] + public IActionResult Analysis(int id) + { + var viewModel = _context.Responses + .Where(r => r.QuestionnaireId == id) + .Include(r => r.Questionnaire) + .ThenInclude(q => q.Questions) + .ThenInclude(q => q.Answers) + .Select(r => new ResponseQuestionnaireWithUsersViewModel + { + Id = r.Questionnaire.Id, + Title = r.Questionnaire.Title, + Description = r.Questionnaire.Description, + UserName = r.UserName, // Assuming you want the user who answered the questionnaire + Email = r.UserEmail, + ParticipantCount = _context.Responses.Count(rs => rs.QuestionnaireId == id), + QuestionsAnsweredPercentage = _context.Questionnaires + .Where(q => q.Id == id) + .SelectMany(q => q.Questions) + .Count() > 0 + ? (double)_context.ResponseDetails + .Where(rd => rd.Response.QuestionnaireId == id && rd.TextResponse != null) + .Select(rd => rd.QuestionId) + .Distinct() + .Count() / _context.Questionnaires + .Where(q => q.Id == id) + .SelectMany(q => q.Questions) + .Count() * 100.0 + : 0.0, // Avoid division by zero + Questions = r.Questionnaire.Questions.Select(q => new ResponseQuestionViewModel + { + Id = q.Id, + Text = q.Text, + Type = q.Type, + Answers = q.Answers.Select(a => new ResponseAnswerViewModel + { + Id = a.Id, + Text = a.Text ?? _context.ResponseDetails + .Where(rd => rd.QuestionId == q.Id && rd.ResponseId == r.Id) + .Select(rd => rd.TextResponse) + .FirstOrDefault(), + Count = _context.ResponseAnswers.Count(ra => ra.AnswerId == a.Id) // Count how many times each answer was selected + }).ToList(), + SelectedAnswerIds = _context.ResponseDetails + .Where(rd => rd.QuestionId == q.Id) + .SelectMany(rd => rd.ResponseAnswers) + .Select(ra => ra.AnswerId) + .Distinct() + .ToList(), + SelectedText = _context.ResponseDetails + .Where(rd => rd.QuestionId == q.Id) + .Select(rd => rd.TextResponse) + .Where(t => !string.IsNullOrEmpty(t)) + .ToList() + }).ToList(), + Users = _context.Responses + .Where(rs => rs.QuestionnaireId == id) + .Select(rs => new ResponseUserViewModel + { + UserName = rs.UserName, + Email = rs.UserEmail + }).Distinct().ToList() + }) + .FirstOrDefault(); + + if (viewModel == null) + { + return NotFound("No questionnaire found for the given ID."); + } + + return View(viewModel); + + } + + + } +} + diff --git a/Web/Areas/Admin/Controllers/UserResponseController.cs b/Web/Areas/Admin/Controllers/UserResponseController.cs index 4801186..a472998 100644 --- a/Web/Areas/Admin/Controllers/UserResponseController.cs +++ b/Web/Areas/Admin/Controllers/UserResponseController.cs @@ -46,5 +46,37 @@ namespace Web.Areas.Admin.Controllers return View(response); // Pass the response to the view } + + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Delete(int id) + { + var response = await _context.Responses.FindAsync(id); + if (response == null) + { + return NotFound(); + } + + _context.Responses.Remove(response); + await _context.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task DeleteMultiple(int[] ids) + { + var responses = _context.Responses.Where(r => ids.Contains(r.Id)); + + _context.Responses.RemoveRange(responses); + await _context.SaveChangesAsync(); + TempData["Success"] = "User response deleted successfully"; + return RedirectToAction(nameof(Index)); + } + + + } } diff --git a/Web/Areas/Admin/Views/Shared/_AdminLayout.cshtml b/Web/Areas/Admin/Views/Shared/_AdminLayout.cshtml index 0ef86b3..1716fd7 100644 --- a/Web/Areas/Admin/Views/Shared/_AdminLayout.cshtml +++ b/Web/Areas/Admin/Views/Shared/_AdminLayout.cshtml @@ -39,6 +39,9 @@ Survey +
  • + Analyzer +
  • Response @@ -46,6 +49,7 @@
  • Subscibers
  • + diff --git a/Web/Areas/Admin/Views/SurveyAnalysis/Analysis.cshtml b/Web/Areas/Admin/Views/SurveyAnalysis/Analysis.cshtml new file mode 100644 index 0000000..d076465 --- /dev/null +++ b/Web/Areas/Admin/Views/SurveyAnalysis/Analysis.cshtml @@ -0,0 +1,133 @@ +@using Newtonsoft.Json +@model ResponseQuestionnaireWithUsersViewModel + +@{ + ViewData["Title"] = "Detailed Survey Analysis"; +} + + + +
    + + +
    +
    + Survey Analyzer +
    +
    +
    @Model.Title
    +

    @Html.Raw(Model.Description)

    + +
    + +
    + +
    + @foreach (var question in Model.Questions) + { + + +
    + +
    +
    + } + +
    +
    + + + + +@section Scripts { + + +} diff --git a/Web/Areas/Admin/Views/SurveyAnalysis/Index.cshtml b/Web/Areas/Admin/Views/SurveyAnalysis/Index.cshtml new file mode 100644 index 0000000..34cc846 --- /dev/null +++ b/Web/Areas/Admin/Views/SurveyAnalysis/Index.cshtml @@ -0,0 +1,64 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Survey Analysis"; +} +
    + + + + +
    +
    Survey analysis
    +
    +

    Survey analysis list

    + + +
    + + + + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + + } + +
    IdQuestionnaireAction
    @item.Id@item.Title + Details +
    + + +
    + +
    +
    +
    + + + +@section Scripts { + + +} diff --git a/Web/Areas/Admin/Views/UserResponse/Index.cshtml b/Web/Areas/Admin/Views/UserResponse/Index.cshtml index 1ea35fc..8a3bcef 100644 --- a/Web/Areas/Admin/Views/UserResponse/Index.cshtml +++ b/Web/Areas/Admin/Views/UserResponse/Index.cshtml @@ -8,6 +8,7 @@
    +
    User Reponse
    @@ -15,42 +16,55 @@

    User response list

    - - - - - - - - - - - - - - - @foreach (var item in Model) - { - - - - - - - - - - } - - - -
    IdQuestionnaireUserNameUserEmailSubmission DateAction
    @item.Id @item.Questionnaire.Title@item.UserName@item.UserName@item.SubmissionDate - Delete | - Details -
    +
    + + + + + + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + + + + } + +
    IdQuestionnaireUserNameUserEmailSubmission DateAction
    @item.Id@item.Questionnaire.Title@item.UserName@item.UserEmail@item.SubmissionDate + Details +
    + + +
    +@section Scripts{ + + +} + diff --git a/Web/Areas/Admin/Views/UserResponse/ViewResponse.cshtml b/Web/Areas/Admin/Views/UserResponse/ViewResponse.cshtml index 8a2c7d4..bf3e75d 100644 --- a/Web/Areas/Admin/Views/UserResponse/ViewResponse.cshtml +++ b/Web/Areas/Admin/Views/UserResponse/ViewResponse.cshtml @@ -21,8 +21,6 @@ } - - .star-rating .text-primary { color:goldenrod; /* Bootstrap primary blue */ font-weight: bold; /* Make text bold */ @@ -182,6 +180,7 @@ white-space: nowrap; vertical-align: baseline; border-radius: 0.25rem; + } @@ -192,9 +191,8 @@
    @*

    Questionnaire: @Model.Questionnaire.Title

    *@ - Response Details for @Model.UserName (@Model.UserEmail) +
    Response Details for @Model.UserName (@Model.UserEmail)
    Submitted on: @Model.SubmissionDate.ToString("yyyy-MM-ddTHH:mm:ss")
    - - @foreach (var detail in Model.ResponseDetails) {
    diff --git a/Web/Controllers/QuestionnaireResponseController.cs b/Web/Controllers/QuestionnaireResponseController.cs index a7e2fbe..b178da4 100644 --- a/Web/Controllers/QuestionnaireResponseController.cs +++ b/Web/Controllers/QuestionnaireResponseController.cs @@ -1,8 +1,10 @@ using Data; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages.Manage; using Model; using Newtonsoft.Json; +using Services.EmailSend; using Services.Interaces; using System.Globalization; using System.Security.Cryptography; @@ -17,11 +19,13 @@ namespace Web.Controllers { private readonly IQuestionnaireRepository _questionnaireRepository; private readonly SurveyContext _context; + private readonly IEmailServices _emailServices; - public QuestionnaireResponseController(IQuestionnaireRepository questionnaireRepository,SurveyContext context) + public QuestionnaireResponseController(IQuestionnaireRepository questionnaireRepository,SurveyContext context, IEmailServices emailServices) { _questionnaireRepository = questionnaireRepository; _context = context; + _emailServices = emailServices; } public IActionResult Index() { @@ -36,7 +40,7 @@ namespace Web.Controllers return View(); } - public IActionResult DisplayQuestionnaire(int id,string t) + public IActionResult DisplayQuestionnaire(int id,string t,string E) { // Check if the token is null or empty if (string.IsNullOrEmpty(t)) @@ -72,6 +76,29 @@ namespace Web.Controllers return RedirectToAction(nameof(Error)); } + bool hasAlreadyResponded = _context.Responses.Any(r => r.QuestionnaireId == id && r.UserEmail == E); + if (hasAlreadyResponded) + { + // Retrieve the first username associated with the email, if available + var userName = _context.Responses.Where(x => x.UserEmail == E) + .Select(x => x.UserName) + .FirstOrDefault(); // This ensures you get a single result or null + + // Ensure userName is not null or empty to use in the message + if (!string.IsNullOrEmpty(userName)) + { + TempData["ErrorMessage"] = $"{userName}"; + } + else + { + TempData["ErrorMessage"] = "You have already taken this survey."; + } + + return RedirectToAction(nameof(SubmittedSurvey)); + } + + + // Retrieve the questionnaire using the numeric ID var questionnaire = _questionnaireRepository.GetQuestionnaireWithQuestionAndAnswer(id); @@ -80,13 +107,9 @@ namespace Web.Controllers [HttpPost] public IActionResult DisplayQuestionnaire([FromForm] ResponseQuestionnaireViewModel questionnaire) { - //bool hasSubmitted = _context.Responses.Any(r => r.QuestionnaireId == questionnaire.Id && r.UserEmail == questionnaire.Email); + bool hasSubmitted = _context.Responses.Any(r => r.QuestionnaireId == questionnaire.Id && r.UserEmail == questionnaire.Email); - //if (hasSubmitted) - //{ - // TempData["ErrorMessage"] = "You have already completed this survey."; - // return RedirectToAction("ThankYou"); - //} + var response = new Response { @@ -111,6 +134,68 @@ namespace Web.Controllers _context.Responses.Add(response); _context.SaveChanges(); + var subject = $"Thank You for Your Feedback, {questionnaire.UserName}!"; + var toEmail = questionnaire.Email; + string emailBody = $@" + + + + + +
    +

    Hey {questionnaire.UserName.ToUpper()},

    +
    {subject}
    +

    Thank you so much for taking the time to provide us with your valuable feedback!

    + +

    If you have any more thoughts to share or need assistance, please don't hesitate to reach out. You can email us directly at seo@seosoft.dk, and we'll be more than happy to help.

    + +

    Thank you once again, {questionnaire.UserName}, for helping us make SeoSoft ApS even better. We truly value your support and participation.

    + + +
    +

    Søren Eggert Lundsteen Olsen
    + Seosoft ApS
    +


    + Hovedgaden 3 + Jordrup
    + Kolding 6064
    + Denmark

    + +
    + + "; + + + // Call the SendConfirmationEmailAsync method to send the email + var emailSend = new EmailToSend(toEmail, subject, emailBody); + + _emailServices.SendConfirmationEmailAsync(emailSend); + TempData["UserName"] = questionnaire.UserName; @@ -125,6 +210,13 @@ namespace Web.Controllers return View(); } + [HttpGet] + public IActionResult SubmittedSurvey() + { + ViewBag.submitedEmail = TempData["ErrorMessage"]; + return View(); + } + private ResponseQuestionnaireViewModel MapToViewModel(Questionnaire questionnaire) { diff --git a/Web/ViewModel/QuestionnaireVM/ResponseAnswerViewModel.cs b/Web/ViewModel/QuestionnaireVM/ResponseAnswerViewModel.cs index 14acf22..1055cc1 100644 --- a/Web/ViewModel/QuestionnaireVM/ResponseAnswerViewModel.cs +++ b/Web/ViewModel/QuestionnaireVM/ResponseAnswerViewModel.cs @@ -4,5 +4,7 @@ { public int Id { get; set; } // Answer ID public string? Text { get; set; } // Answer text + + public int? Count { get; set; } } } diff --git a/Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireViewModel.cs b/Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireViewModel.cs index 8b08f57..115e64b 100644 --- a/Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireViewModel.cs +++ b/Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireViewModel.cs @@ -1,5 +1,5 @@ using System.ComponentModel.DataAnnotations; -using Web.ViewModel.QuestionVM; + namespace Web.ViewModel.QuestionnaireVM { @@ -15,10 +15,11 @@ namespace Web.ViewModel.QuestionnaireVM [Required] public string? Email { get; set; } + public int ParticipantCount { get; set; } + public int QuestionsAnsweredCount { get; set; } - - // Collection of questions - public List Questions { get; set; } = new List(); + + public List Questions { get; set; } = new List(); } diff --git a/Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireWithUsersViewModel.cs b/Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireWithUsersViewModel.cs new file mode 100644 index 0000000..7343c91 --- /dev/null +++ b/Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireWithUsersViewModel.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +namespace Web.ViewModel.QuestionnaireVM +{ + public class ResponseQuestionnaireWithUsersViewModel + { + public int Id { get; set; } // Questionnaire ID + public string? Title { get; set; } // Title of the questionnaire + public string? Description { get; set; } // Description of the questionnaire + + [Required] + public string? UserName { get; set; } + + [Required] + public string? Email { get; set; } + public int ParticipantCount { get; set; } + public double QuestionsAnsweredPercentage { get; set; } + + public List Questions { get; set; } = new List(); + + public List Users { get; set; }=new List { }; + + } +} diff --git a/Web/ViewModel/QuestionnaireVM/ResponseUserViewModel.cs b/Web/ViewModel/QuestionnaireVM/ResponseUserViewModel.cs new file mode 100644 index 0000000..5263955 --- /dev/null +++ b/Web/ViewModel/QuestionnaireVM/ResponseUserViewModel.cs @@ -0,0 +1,8 @@ +namespace Web.ViewModel.QuestionnaireVM +{ + public class ResponseUserViewModel + { + public string? UserName { get; set; } + public string? Email { get; set; } + } +} diff --git a/Web/Views/QuestionnaireResponse/SubmittedSurvey.cshtml b/Web/Views/QuestionnaireResponse/SubmittedSurvey.cshtml new file mode 100644 index 0000000..3fb7b8f --- /dev/null +++ b/Web/Views/QuestionnaireResponse/SubmittedSurvey.cshtml @@ -0,0 +1,153 @@ + +@{ + ViewData["Title"] = "SubmittedSurvey"; + Layout = "~/Views/Shared/_QuestionnaireResponse.cshtml"; +} + + + + +
    + + +
    +
    + + +
    +
    +
    Thank You, @ViewBag.submitedEmail, you have already taken the survey.
    + +
    +
    + + +
    +
    + + +
    +
    + +
    + diff --git a/Web/Views/QuestionnaireResponse/ThankYou.cshtml b/Web/Views/QuestionnaireResponse/ThankYou.cshtml index 8161dab..f366549 100644 --- a/Web/Views/QuestionnaireResponse/ThankYou.cshtml +++ b/Web/Views/QuestionnaireResponse/ThankYou.cshtml @@ -135,12 +135,10 @@
    -

    Hey @ViewBag.UserName

    - - - -
    Thank you for taking the time to submit the form! Your input is invaluable to us and will help us improve our services.
    +

    Hello @ViewBag.UserName

    +
    Thank you for taking the time to submit the survey!
    +

    Your insights are invaluable to us and help us improve our services.

    diff --git a/Web/appsettings.json b/Web/appsettings.json index 58be4eb..9e950a2 100644 --- a/Web/appsettings.json +++ b/Web/appsettings.json @@ -16,7 +16,7 @@ "ConfirmEmailPath": "Subscription/Confirmation", "unsubscribePath": "Subscription/UnsubscribeConfirmation", "Questionnaire": "QuestionnaireResponse/DisplayQuestionnaire", - "EncryptionKey": "" + }, "MailJet": {