diff --git a/Model/SentQuestionnaire.cs b/Model/SentQuestionnaire.cs new file mode 100644 index 0000000..37880fa --- /dev/null +++ b/Model/SentQuestionnaire.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +namespace Model +{ + public class SentQuestionnaire + { + public int Id { get; set; } + public string? Name { get; set; } + public string? Email { get; set; } + + public int QuestionnaireId { get; set; } + [ForeignKey("QuestionnaireId")] + public Questionnaire? Questionnaire { get; set; } + } +} diff --git a/Web/Areas/Admin/Controllers/QuestionnaireController.cs b/Web/Areas/Admin/Controllers/QuestionnaireController.cs index e341206..0644cfc 100644 --- a/Web/Areas/Admin/Controllers/QuestionnaireController.cs +++ b/Web/Areas/Admin/Controllers/QuestionnaireController.cs @@ -4,10 +4,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.WebUtilities; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Elfie.Extensions; using Microsoft.IdentityModel.Tokens; using Model; using Services.EmailSend; using Services.Interaces; +using System.Globalization; using System.Security.Cryptography; using System.Text; using Web.ViewModel.QuestionnaireVM; @@ -23,7 +25,7 @@ namespace Web.Areas.Admin.Controllers private readonly IConfiguration _configuration; private readonly IEmailServices _emailServices; - public QuestionnaireController(IQuestionnaireRepository Questionnaire, SurveyContext Context, IQuestionRepository Question,IConfiguration configuration,IEmailServices emailServices) + public QuestionnaireController(IQuestionnaireRepository Questionnaire, SurveyContext Context, IQuestionRepository Question, IConfiguration configuration, IEmailServices emailServices) { _questionnaire = Questionnaire; _context = Context; @@ -222,7 +224,7 @@ namespace Web.Areas.Admin.Controllers if (!viewModel.Questions.Any(q => q.Id == existingQuestion.Id)) { existingQuestionnaire.Questions.Remove(existingQuestion); - + } await _questionnaire.Update(existingQuestionnaire); } @@ -241,7 +243,7 @@ namespace Web.Areas.Admin.Controllers { var existingQuestion = existingQuestionnaire.Questions.FirstOrDefault(q => q.Id == questionViewModel.Id); - if(questionViewModel.Id !=0) + if (questionViewModel.Id != 0) { if (existingQuestion != null) { @@ -282,7 +284,7 @@ namespace Web.Areas.Admin.Controllers } } - + else { // Create a new question @@ -328,7 +330,7 @@ namespace Web.Areas.Admin.Controllers } - + await _questionnaire.Update(existingQuestionnaire); TempData["Success"] = "Questionnaire updated successfully"; @@ -403,7 +405,7 @@ namespace Web.Areas.Admin.Controllers - + } @@ -461,20 +463,40 @@ namespace Web.Areas.Admin.Controllers { if (ModelState.IsValid) { - Guid guid = Guid.NewGuid(); - - // Convert the GUID to a string - string guidString = guid.ToString(); - - // Construct the complete URL with the GUID-style ID - + // Build the email body with questionnaire details var questionnairePath = _configuration["Email:Questionnaire"]; int surveyId = viewModel.QuestionnaireId; - var completeUrl = $"{Request.Scheme}://{Request.Host}/{questionnairePath}/{viewModel.QuestionnaireId}"; + //DateTime currentDateTime = DateTime.Now; + //DateTime currentDateTime = viewModel.ExpirationDateTime; + DateTime currentDateTime; + if (viewModel.ExpirationDateTime.HasValue) + { + currentDateTime = viewModel.ExpirationDateTime.Value; + } + else + { + // Handle the case when ExpirationDateTime is null + // For example, you can assign the current date and time + currentDateTime = DateTime.Now; + } + // Calculate the expiration date and time by adding 5 minutes to the current date and time + DateTime expiryDateTime = currentDateTime; + + // Generate a unique token, for example, using a cryptographic library or a GUID + string token = Guid.NewGuid().ToString(); + + // 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}?token={tokenWithExpiry}"; + + //var completeUrl = $"{Request.Scheme}://{Request.Host}/{questionnairePath}/{viewModel.QuestionnaireId}"; var toEmail = viewModel.Email; @@ -491,6 +513,9 @@ namespace Web.Areas.Admin.Controllers body {{ font-family: Arial, sans-serif; }} + .text-danger {{ + color:red; + }} .container {{ max-width: 600px; margin: 0 auto; @@ -517,12 +542,14 @@ namespace Web.Areas.Admin.Controllers

Hey {viewModel.Name},

{subject}

Thank you for participating in our survey. Your feedback is valuable to us.

-

Please click the button below to start the survey:


+

Please click the button below to start the survey:

+

The survey will be expire in Date:{expiryDateTime.ToLongDateString()} Time: {expiryDateTime.ToShortTimeString()}

Start Survey +
+

- -

Søren Eggert Lundsteen Olsen
+

Søren Eggert Lundsteen Olsen Seosoft ApS


Hovedgaden 3 @@ -558,6 +585,18 @@ namespace Web.Areas.Admin.Controllers // If model state is not valid, return the view with validation errors return View(viewModel); } + public string GenerateExpiryToken(DateTime expiryDate) + { + // Generate a unique token, for example, using a cryptographic library or a GUID + string token = Guid.NewGuid().ToString(); + + // Append the expiration date to the token (you might want to encrypt it for security) + string tokenWithExpiry = $"{token}|{expiryDate.ToString("yyyy-MM-ddTHH:mm:ssZ")}"; + + return tokenWithExpiry; + } + + } } diff --git a/Web/Areas/Admin/Views/Questionnaire/SendQuestionnaire.cshtml b/Web/Areas/Admin/Views/Questionnaire/SendQuestionnaire.cshtml index ed1e4f3..6889216 100644 --- a/Web/Areas/Admin/Views/Questionnaire/SendQuestionnaire.cshtml +++ b/Web/Areas/Admin/Views/Questionnaire/SendQuestionnaire.cshtml @@ -30,6 +30,11 @@ +
+ + + +
diff --git a/Web/Controllers/QuestionnaireResponseController.cs b/Web/Controllers/QuestionnaireResponseController.cs index 75c4a3e..e269b6f 100644 --- a/Web/Controllers/QuestionnaireResponseController.cs +++ b/Web/Controllers/QuestionnaireResponseController.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Mvc; using Services.Interaces; +using System.Globalization; using System.Security.Cryptography; using System.Text; +using Web.ViewModel.QuestionnaireVM; namespace Web.Controllers { @@ -15,20 +17,102 @@ namespace Web.Controllers } public IActionResult Index() { + return View(); } - public IActionResult DisplayQuestionnaire(int id) + public IActionResult Error() { + ViewBag.ErrorMessage = "The survey link has expired. request a new link."; + + return View(); + } + + public IActionResult DisplayQuestionnaire(int id, string token) + { + // Check if the token is null or empty + if (string.IsNullOrEmpty(token)) + { + ViewBag.ErrorMessage = "The URL is invalid. Please provide a valid token."; + return View("Error"); + } + + // Split the token to extract the expiration date and time + string[] tokenParts = token.Split('|'); + if (tokenParts.Length != 2) + { + ViewBag.ErrorMessage = "The URL is invalid. Please provide a valid token."; + return View("Error"); + } + + string expiryDateTimeString = tokenParts[1]; + + // Parse the expiration datetime in UTC format + if (!DateTime.TryParseExact(expiryDateTimeString, "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime expiryDateTimeUtc)) + { + ViewBag.ErrorMessage = "The URL is invalid. Please provide a valid token."; + return View("Error"); + } + + // Convert the expiration datetime to local time + DateTime expiryDateTimeLocal = expiryDateTimeUtc.ToLocalTime(); + + // Check if the token is expired (accounting for UTC+2 offset) + if (expiryDateTimeLocal < DateTime.Now.AddHours(2)) + { + + return RedirectToAction(nameof(Error)); + } // Retrieve the questionnaire using the numeric ID var questionnaire = _questionnaireRepository.GetQuestionnaireWithQuestionAndAnswer(id); - - - // Display the questionnaire + return View(questionnaire); } - + + //public IActionResult DisplayQuestionnaire(int id, string token) + //{ + // // Check if the token is null or empty + // if (string.IsNullOrEmpty(token)) + // { + // ViewBag.ErrorMessage = "The URL is invalid. Please provide a valid token."; + // return View("Error"); + // } + + // // Split the token to extract the expiration date and time + // string[] tokenParts = token.Split('|'); + // if (tokenParts.Length != 2) + // { + // ViewBag.ErrorMessage = "The URL is invalid. Please provide a valid token."; + // return View("Error"); + // } + + // string expiryDateTimeString = tokenParts[1]; + + // // Parse the expiration datetime in UTC format + // if (!DateTime.TryParseExact(expiryDateTimeString, "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime expiryDateTimeUtc)) + // { + // ViewBag.ErrorMessage = "The URL is invalid. Please provide a valid token."; + // return View("Error"); + // } + + // // Convert the expiration datetime to local time + // DateTime expiryDateTimeLocal = expiryDateTimeUtc.ToLocalTime(); + + // // Check if the token is expired (accounting for time zone offset) + // if (expiryDateTimeLocal >= DateTime.Now.AddHours(1)) + // { + + // return RedirectToAction(nameof(Error)); + // } + + // // Retrieve the questionnaire using the numeric ID + // var questionnaire = _questionnaireRepository.GetQuestionnaireWithQuestionAndAnswer(id); + + // return View(questionnaire); + //} + + } } diff --git a/Web/ViewModel/QuestionnaireVM/SendQuestionnaireViewModel.cs b/Web/ViewModel/QuestionnaireVM/SendQuestionnaireViewModel.cs index 5bc1340..722704f 100644 --- a/Web/ViewModel/QuestionnaireVM/SendQuestionnaireViewModel.cs +++ b/Web/ViewModel/QuestionnaireVM/SendQuestionnaireViewModel.cs @@ -1,4 +1,5 @@ using NuGet.Protocol.Core.Types; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Web.ViewModel.QuestionnaireVM @@ -12,6 +13,10 @@ namespace Web.ViewModel.QuestionnaireVM [Required] public string? Email { get; set; } + [Required] + [DisplayName("Set expiration date and time for the URL")] + public DateTime? ExpirationDateTime { get; set; } + public int QuestionnaireId { get; set; } } diff --git a/Web/Views/QuestionnaireResponse/DisplayQuestionnaire.cshtml b/Web/Views/QuestionnaireResponse/DisplayQuestionnaire.cshtml index a7fda96..1c04715 100644 --- a/Web/Views/QuestionnaireResponse/DisplayQuestionnaire.cshtml +++ b/Web/Views/QuestionnaireResponse/DisplayQuestionnaire.cshtml @@ -3,128 +3,192 @@ ViewData["Title"] = "DisplayQuestionnaire"; Layout = "~/Views/Shared/_QuestionnaireResponse.cshtml"; } - -
-
-
-

@Html.Raw(Model.Description)

- @{ - int questionNumber = 1; // Counter for question numbers, starting from 1 - } +
+
+
-
-
- @foreach (var question in Model.Questions) - { -
@questionNumber. @question.Text
-
- @if (question.Type == QuestionType.Multiple_choice) +

@ViewBag.ErrorMessage

+ +

@Model.Title

+

@Html.Raw(Model.Description)

+
+
+
+ +
+
+ @for (int i = 0; i < Model.Questions.Count; i++) { - @foreach (var answer in question.Answers) - { - -
- } - } - else if (question.Type == QuestionType.Ranking) - { - - } - else if (question.Type == QuestionType.TrueFalse) - { - -
- - } - else if (question.Type == QuestionType.Rating) - { -
- @for (int i = 1; i <= question.Answers.Count; i++) - { - - - } + var question = Model.Questions[i]; + string stepClass = i == 0 ? "active" : ""; // Adjusted the index to start from the first question +
+ @((i + 1)). + @question.Type
} - else if (question.Type == QuestionType.Text) - { - - } - else if (question.Type == QuestionType.Slider) - { - - 0 - } -
-
- @@questionNumber - ++; // Increment question number after displaying - } - +
+ +
+ + @for (int i = 0; i < Model.Questions.Count; i++) + { + var question = Model.Questions[i]; +
+

@(i + 1). @question.Text

+ @switch (question.Type) + { + case QuestionType.Text: +
+ +
+ break; + case QuestionType.CheckBox: + case QuestionType.Multiple_choice: + case QuestionType.Rating: + case QuestionType.Likert: + case QuestionType.Matrix: + case QuestionType.Demographic: + case QuestionType.Ranking: +
+ @foreach (var answer in question.Answers) + { +
+ + +
+ } +
+ break; + case QuestionType.TrueFalse: +
+ + +
+
+ + +
+ break; + case QuestionType.Open_ended: + + break; + case QuestionType.Image: + + break; + case QuestionType.Slider: + + 50 + + break; + default: + + break; + } +
+ @if (i > 0) + { + + } + @if (i < Model.Questions.Count - 1) + { + + } +
+ +
+ } + + + +
- - - - - - +
-
- -
+ @@ -135,14 +199,138 @@ @{ } - + @* *@ } diff --git a/Web/Views/QuestionnaireResponse/Error.cshtml b/Web/Views/QuestionnaireResponse/Error.cshtml new file mode 100644 index 0000000..249ae41 --- /dev/null +++ b/Web/Views/QuestionnaireResponse/Error.cshtml @@ -0,0 +1,73 @@ + +@{ + ViewData["Title"] = "Error"; + Layout = "~/Views/Shared/_QuestionnaireResponse.cshtml"; +} + + +
+
+
+
+ +
+
@ViewBag.ErrorMessage
+ Contact +
+ +
+ + +
+
+ +
+ +
+ + +
+ + + + + + diff --git a/Web/wwwroot/css/site.css b/Web/wwwroot/css/site.css index beb0253..ea42079 100644 --- a/Web/wwwroot/css/site.css +++ b/Web/wwwroot/css/site.css @@ -343,6 +343,7 @@ body, html { color: #6c757d !important; transition: 394ms; } + /*_______________________________________________________________________________end of the custom CSS_____________________________________________________________*/ *, *::before, @@ -370,7 +371,7 @@ body, html { line-height: 1.5; color: #212529; text-align: left; - background-color: #dfdfdf !important; + } [tabindex="-1"]:focus {