From f6a03302fd7818623e69d24136c50883fc6219d7 Mon Sep 17 00:00:00 2001 From: Qaisyousuf Date: Mon, 11 Aug 2025 18:06:13 +0200 Subject: [PATCH] Add backend condition logic for user questionnaire submission handling --- Model/ResponseDetail.cs | 10 + .../UserResponseStatusController.cs | 3 +- .../UserResponsesStatus.cshtml | 234 +++-- .../QuestionnaireResponseController.cs | 148 ++- ...250811130002_AddResponseStatus.Designer.cs | 841 ++++++++++++++++++ .../20250811130002_AddResponseStatus.cs | 39 + Web/Migrations/SurveyContextModelSnapshot.cs | 6 + .../ResponseQuestionnaireViewModel.cs | 3 +- .../DisplayQuestionnaire.cshtml | 66 +- 9 files changed, 1236 insertions(+), 114 deletions(-) create mode 100644 Web/Migrations/20250811130002_AddResponseStatus.Designer.cs create mode 100644 Web/Migrations/20250811130002_AddResponseStatus.cs diff --git a/Model/ResponseDetail.cs b/Model/ResponseDetail.cs index d56daeb..46a3c32 100644 --- a/Model/ResponseDetail.cs +++ b/Model/ResponseDetail.cs @@ -23,5 +23,15 @@ namespace Model public QuestionType QuestionType { get; set; } public string? TextResponse { get; set; } public List ResponseAnswers { get; set; } = new List(); + + public ResponseStatus Status { get; set; } = ResponseStatus.Shown; + public string? SkipReason { get; set; } // Why it was skipped (JSON of condition) + } + + public enum ResponseStatus + { + Answered = 1, // Question was answered + Shown = 2, // Question was shown but left blank + Skipped = 3 // Question was skipped due to conditional logic } } diff --git a/Web/Areas/Admin/Controllers/UserResponseStatusController.cs b/Web/Areas/Admin/Controllers/UserResponseStatusController.cs index 4c4e27d..375d3ac 100644 --- a/Web/Areas/Admin/Controllers/UserResponseStatusController.cs +++ b/Web/Areas/Admin/Controllers/UserResponseStatusController.cs @@ -46,9 +46,10 @@ namespace Web.Areas.Admin.Controllers { 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 the Answers entity + .ThenInclude(q => q.Answers) .Include(r => r.ResponseDetails) .ThenInclude(rd => rd.ResponseAnswers) .Where(r => r.UserEmail == userEmail) diff --git a/Web/Areas/Admin/Views/UserResponseStatus/UserResponsesStatus.cshtml b/Web/Areas/Admin/Views/UserResponseStatus/UserResponsesStatus.cshtml index 66a1ba3..85434cd 100644 --- a/Web/Areas/Admin/Views/UserResponseStatus/UserResponsesStatus.cshtml +++ b/Web/Areas/Admin/Views/UserResponseStatus/UserResponsesStatus.cshtml @@ -5,8 +5,6 @@ ViewData["Title"] = "User Responses"; } - - - -

Back to list @@ -77,7 +107,7 @@

- +
@Model.UserName (@Model.UserEmail)
@@ -90,21 +120,17 @@ Excel Reports
- +
- +

- - -
-
@@ -141,64 +167,92 @@
-
- - -
+ - @foreach (var detail in response.ResponseDetails) + @foreach (var question in response.Questionnaire.Questions.OrderBy(q => q.Id)) { + var responseDetail = response.ResponseDetails.FirstOrDefault(rd => rd.QuestionId == question.Id); + - + + @@ -207,7 +261,6 @@
QuestionStatus Response
@detail.Question.Text - @if (detail.QuestionType == QuestionType.Text || detail.QuestionType == QuestionType.Slider || detail.QuestionType == QuestionType.Open_ended) + @question.Text +
+ Type: @question.Type +
+ @if (responseDetail != null) { -
    -
  • - Question type - @detail.QuestionType -
  • -
-
-
    -
  • - Answer - @detail.TextResponse -
  • -
+ @switch (responseDetail.Status) + { + case ResponseStatus.Answered: +
+ ✅ Answered +
+ break; + + case ResponseStatus.Shown: +
+ ⚠️ Shown but not answered +
+ Question was displayed but left blank +
+ break; + + case ResponseStatus.Skipped: +
+ ⏭️ Skipped +
+ @responseDetail.SkipReason +
+ break; + } } else { -
    -
  • - Question type - @detail.QuestionType -
  • -
-
-
    - @foreach (var answer in detail.ResponseAnswers) - { -
  • - Answer - @detail.Question.Answers.FirstOrDefault(a => a.Id == answer.AnswerId)?.Text -
  • - } -
+
+ ❓ No response data +
+ Status unknown +
+ } +
+ @if (responseDetail != null && responseDetail.Status == ResponseStatus.Answered) + { + @if (responseDetail.QuestionType == QuestionType.Text || responseDetail.QuestionType == QuestionType.Slider || responseDetail.QuestionType == QuestionType.Open_ended) + { +
+ Answer: @responseDetail.TextResponse +
+ } + else if (responseDetail.ResponseAnswers.Any()) + { +
+ @foreach (var answer in responseDetail.ResponseAnswers) + { + var answerText = question.Answers.FirstOrDefault(a => a.Id == answer.AnswerId)?.Text; + @answerText + } +
+ } + } + else + { + No response provided }
- } @@ -215,13 +268,10 @@ - - - @section Scripts { - + -} +} \ No newline at end of file diff --git a/Web/Controllers/QuestionnaireResponseController.cs b/Web/Controllers/QuestionnaireResponseController.cs index 9bba2c2..16b7d03 100644 --- a/Web/Controllers/QuestionnaireResponseController.cs +++ b/Web/Controllers/QuestionnaireResponseController.cs @@ -1,19 +1,16 @@ using Data; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; -using Microsoft.EntityFrameworkCore; -using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages.Manage; using Model; using Newtonsoft.Json; using Services.EmailSend; using Services.Implemnetation; using Services.Interaces; using System.Globalization; -using System.Security.Cryptography; -using System.Text; -using Web.ViewModel.AnswerVM; +using System.Text.Json; +using System.Text.Json.Serialization; using Web.ViewModel.QuestionnaireVM; -using Web.ViewModel.QuestionVM; + namespace Web.Controllers { @@ -110,9 +107,14 @@ namespace Web.Controllers } [HttpPost] public IActionResult DisplayQuestionnaire([FromForm] ResponseQuestionnaireViewModel questionnaire) + { + bool hasSubmitted = _context.Responses.Any(r => r.QuestionnaireId == questionnaire.Id && r.UserEmail == questionnaire.Email); + // Get the actual questionnaire from database + var dbQuestionnaire = _questionnaireRepository.GetQuestionnaireWithQuestionAndAnswer(questionnaire.Id); + var cetZone = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); var cetTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, cetZone); var response = new Response @@ -121,17 +123,7 @@ namespace Web.Controllers UserName = questionnaire.UserName, UserEmail = questionnaire.Email, SubmissionDate = cetTime, - ResponseDetails = questionnaire.Questions.Select(q => new ResponseDetail - { - QuestionId = q.Id, - QuestionType = q.Type, - TextResponse = (q.Type == QuestionType.Open_ended || q.Type == QuestionType.Text || q.Type == QuestionType.Slider) - ? string.Join(" ", q.SelectedText) - : null, - ResponseAnswers = q.SelectedAnswerIds - .Select(aid => new ResponseAnswer { AnswerId = aid }) - .ToList() - }).ToList() + ResponseDetails = CreateResponseDetailsForAllQuestions(dbQuestionnaire, questionnaire.Questions, questionnaire.QuestionsShown, questionnaire.QuestionsSkipped) }; _context.Responses.Add(response); @@ -166,6 +158,128 @@ namespace Web.Controllers return RedirectToAction(nameof(ThankYou)); } + private List CreateResponseDetailsForAllQuestions(Questionnaire questionnaire, List answeredQuestions, string questionsShownJson, string questionsSkippedJson) + { + var responseDetails = new List(); + + // Parse tracking data + List questionsShown = new List(); + List questionsSkipped = new List(); + + try + { + if (!string.IsNullOrEmpty(questionsShownJson)) + { + questionsShown = System.Text.Json.JsonSerializer.Deserialize>(questionsShownJson) ?? new List(); + } + + if (!string.IsNullOrEmpty(questionsSkippedJson)) + { + questionsSkipped = System.Text.Json.JsonSerializer.Deserialize>(questionsSkippedJson) ?? new List(); + } + } + catch (System.Text.Json.JsonException ex) + { + // Log error if needed + Console.WriteLine($"Error parsing tracking data: {ex.Message}"); + } + + // Get ALL questions from the questionnaire + var allQuestions = questionnaire.Questions.OrderBy(q => q.Id).ToList(); + + foreach (var dbQuestion in allQuestions) + { + var questionNumber = GetQuestionNumber(dbQuestion.Id, allQuestions); + var answeredQuestion = answeredQuestions.FirstOrDefault(aq => aq.Id == dbQuestion.Id); + var skippedInfo = questionsSkipped.FirstOrDefault(sq => sq.QuestionNumber == questionNumber); + + ResponseDetail responseDetail; + + if (answeredQuestion != null && HasValidResponse(answeredQuestion)) + { + // Question was ANSWERED + responseDetail = new ResponseDetail + { + QuestionId = dbQuestion.Id, + QuestionType = dbQuestion.Type, + Status = ResponseStatus.Answered, + TextResponse = (dbQuestion.Type == QuestionType.Open_ended || + dbQuestion.Type == QuestionType.Text || + dbQuestion.Type == QuestionType.Slider) + ? string.Join(" ", answeredQuestion.SelectedText) + : null, + ResponseAnswers = answeredQuestion.SelectedAnswerIds + .Select(aid => new ResponseAnswer { AnswerId = aid }) + .ToList() + }; + } + else if (skippedInfo != null) + { + // Question was SKIPPED due to conditional logic + responseDetail = new ResponseDetail + { + QuestionId = dbQuestion.Id, + QuestionType = dbQuestion.Type, + Status = ResponseStatus.Skipped, + SkipReason = skippedInfo.Reason + }; + } + else if (questionsShown.Contains(questionNumber)) + { + // Question was SHOWN but left blank + responseDetail = new ResponseDetail + { + QuestionId = dbQuestion.Id, + QuestionType = dbQuestion.Type, + Status = ResponseStatus.Shown, + SkipReason = null + }; + } + else + { + // Fallback - assume shown if no tracking data available + responseDetail = new ResponseDetail + { + QuestionId = dbQuestion.Id, + QuestionType = dbQuestion.Type, + Status = ResponseStatus.Shown, + SkipReason = null + }; + } + + responseDetails.Add(responseDetail); + } + + return responseDetails; + } + + private int GetQuestionNumber(int questionId, List allQuestions) + { + return allQuestions.FindIndex(q => q.Id == questionId) + 1; + } + + private bool HasValidResponse(ResponseQuestionViewModel question) + { + bool hasTextResponse = question.SelectedText != null && + question.SelectedText.Any(text => !string.IsNullOrWhiteSpace(text)); + + bool hasAnswerResponse = question.SelectedAnswerIds != null && + question.SelectedAnswerIds.Any(); + + return hasTextResponse || hasAnswerResponse; + } + + // Add this class for JSON deserialization + public class SkippedQuestionInfo + { + [JsonPropertyName("questionNumber")] + public int QuestionNumber { get; set; } + + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; + } + + // ✅ COMPLETE CORRECTED METHOD: Danish Thank You Email Body private static string GenerateThankYouEmailBody(string userName) { diff --git a/Web/Migrations/20250811130002_AddResponseStatus.Designer.cs b/Web/Migrations/20250811130002_AddResponseStatus.Designer.cs new file mode 100644 index 0000000..51c4690 --- /dev/null +++ b/Web/Migrations/20250811130002_AddResponseStatus.Designer.cs @@ -0,0 +1,841 @@ +// +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("20250811130002_AddResponseStatus")] + partial class AddResponseStatus + { + /// + 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("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Model.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CVR") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Mobile") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresss"); + }); + + modelBuilder.Entity("Model.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConditionJson") + .HasColumnType("nvarchar(max)"); + + b.Property("QuestionId") + .HasColumnType("int"); + + b.Property("Text") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.ToTable("Answers"); + }); + + modelBuilder.Entity("Model.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ImageUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LinkUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Banners"); + }); + + modelBuilder.Entity("Model.Footer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ImageUlr") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastUpdated") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Owner") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Sitecopyright") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Footers"); + }); + + modelBuilder.Entity("Model.FooterSocialMedia", b => + { + b.Property("FooterId") + .HasColumnType("int"); + + b.Property("SocialId") + .HasColumnType("int"); + + b.HasKey("FooterId", "SocialId"); + + b.HasIndex("SocialId"); + + b.ToTable("FooterSocialMedias"); + }); + + modelBuilder.Entity("Model.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BannerId") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FooterId") + .HasColumnType("int"); + + b.Property("Slug") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BannerId"); + + b.HasIndex("FooterId"); + + b.ToTable("Pages"); + }); + + modelBuilder.Entity("Model.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("QuestionnaireId") + .HasColumnType("int"); + + b.Property("Text") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("QuestionnaireId"); + + b.ToTable("Questions"); + }); + + modelBuilder.Entity("Model.Questionnaire", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Questionnaires"); + }); + + modelBuilder.Entity("Model.Response", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("QuestionnaireId") + .HasColumnType("int"); + + b.Property("SubmissionDate") + .HasColumnType("datetime2"); + + b.Property("UserEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("QuestionnaireId"); + + b.ToTable("Responses"); + }); + + modelBuilder.Entity("Model.ResponseAnswer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerId") + .HasColumnType("int"); + + b.Property("ResponseDetailId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ResponseDetailId"); + + b.ToTable("ResponseAnswers"); + }); + + modelBuilder.Entity("Model.ResponseDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("QuestionId") + .HasColumnType("int"); + + b.Property("QuestionType") + .HasColumnType("int"); + + b.Property("ResponseId") + .HasColumnType("int"); + + b.Property("SkipReason") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TextResponse") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ResponseId"); + + b.ToTable("ResponseDetails"); + }); + + modelBuilder.Entity("Model.SentNewsletterEamil", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .HasColumnType("nvarchar(max)"); + + b.Property("Geo") + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("IsBlocked") + .HasColumnType("bit"); + + b.Property("IsBounced") + .HasColumnType("bit"); + + b.Property("IsClicked") + .HasColumnType("bit"); + + b.Property("IsDelivered") + .HasColumnType("bit"); + + b.Property("IsOpened") + .HasColumnType("bit"); + + b.Property("IsSent") + .HasColumnType("bit"); + + b.Property("IsSpam") + .HasColumnType("bit"); + + b.Property("IsUnsubscribed") + .HasColumnType("bit"); + + b.Property("ReceivedActivity") + .HasColumnType("datetime2"); + + b.Property("RecipientEmail") + .HasColumnType("nvarchar(max)"); + + b.Property("SentDate") + .HasColumnType("datetime2"); + + b.Property("Subject") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("SentNewsletterEamils"); + }); + + modelBuilder.Entity("Model.SocialMedia", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("SocialMedia"); + }); + + modelBuilder.Entity("Model.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsSubscribed") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Model.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Model.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", 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", 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 + } + } +} diff --git a/Web/Migrations/20250811130002_AddResponseStatus.cs b/Web/Migrations/20250811130002_AddResponseStatus.cs new file mode 100644 index 0000000..0a9165e --- /dev/null +++ b/Web/Migrations/20250811130002_AddResponseStatus.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Web.Migrations +{ + /// + public partial class AddResponseStatus : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SkipReason", + table: "ResponseDetails", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "Status", + table: "ResponseDetails", + type: "int", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SkipReason", + table: "ResponseDetails"); + + migrationBuilder.DropColumn( + name: "Status", + table: "ResponseDetails"); + } + } +} diff --git a/Web/Migrations/SurveyContextModelSnapshot.cs b/Web/Migrations/SurveyContextModelSnapshot.cs index edc09af..9fb723a 100644 --- a/Web/Migrations/SurveyContextModelSnapshot.cs +++ b/Web/Migrations/SurveyContextModelSnapshot.cs @@ -531,6 +531,12 @@ namespace Web.Migrations b.Property("ResponseId") .HasColumnType("int"); + b.Property("SkipReason") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + b.Property("TextResponse") .HasColumnType("nvarchar(max)"); diff --git a/Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireViewModel.cs b/Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireViewModel.cs index 115e64b..fa7f05b 100644 --- a/Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireViewModel.cs +++ b/Web/ViewModel/QuestionnaireVM/ResponseQuestionnaireViewModel.cs @@ -20,7 +20,8 @@ namespace Web.ViewModel.QuestionnaireVM public List Questions { get; set; } = new List(); - + public string QuestionsShown { get; set; } = string.Empty; + public string QuestionsSkipped { get; set; } = string.Empty; } } diff --git a/Web/Views/QuestionnaireResponse/DisplayQuestionnaire.cshtml b/Web/Views/QuestionnaireResponse/DisplayQuestionnaire.cshtml index 3be82b2..e9809e3 100644 --- a/Web/Views/QuestionnaireResponse/DisplayQuestionnaire.cshtml +++ b/Web/Views/QuestionnaireResponse/DisplayQuestionnaire.cshtml @@ -314,6 +314,13 @@
+ + + + + + + @@ -680,9 +687,32 @@ return; } - // Track the actual navigation path + // Track which questions were shown and skipped - FIXED VARIABLE NAMES + let questionsShownArray = [1]; // Start with question 1 + let questionsSkippedArray = []; let navigationPath = [0]; // Start with question 1 (index 0) + function trackQuestionShown(questionNumber) { + if (!questionsShownArray.includes(questionNumber)) { + questionsShownArray.push(questionNumber); + console.log('Question shown:', questionNumber, 'Total shown:', questionsShownArray); + } + } + + function trackQuestionSkipped(questionNumber, reason) { + const skipInfo = { + questionNumber: questionNumber, + reason: reason + }; + questionsSkippedArray.push(skipInfo); + console.log('Question skipped:', skipInfo); + } + + function updateTrackingInputs() { + document.getElementById('questionsShown').value = JSON.stringify(questionsShownArray); + document.getElementById('questionsSkipped').value = JSON.stringify(questionsSkippedArray); + } + // Get conditions from the CURRENT step only function getCurrentStepConditions() { const currentStepElement = steps[currentStep]; @@ -732,8 +762,16 @@ function jumpToQuestion(questionNumber) { const targetStepIndex = questionNumber - 1; // Convert to 0-based index if (targetStepIndex >= 0 && targetStepIndex < steps.length) { + const currentQuestionNumber = getCurrentStepNumber(); + + // Track skipped questions between current and target + for (let i = currentQuestionNumber + 1; i < questionNumber; i++) { + trackQuestionSkipped(i, `Skipped due to jump condition from question ${currentQuestionNumber}`); + } + currentStep = targetStepIndex; navigationPath.push(currentStep); // Track the path + trackQuestionShown(questionNumber); // Track the target question as shown showStep(currentStep); updateStepper(); console.log(`Jumped to question ${questionNumber}, path:`, navigationPath); @@ -741,10 +779,21 @@ } function skipQuestions(skipCount) { + const currentQuestionNumber = getCurrentStepNumber(); const newStepIndex = currentStep + skipCount + 1; + + // Track skipped questions + for (let i = 1; i <= skipCount; i++) { + const skippedQuestionNumber = currentQuestionNumber + i; + if (skippedQuestionNumber <= steps.length) { + trackQuestionSkipped(skippedQuestionNumber, `Skipped due to skip condition from question ${currentQuestionNumber}`); + } + } + if (newStepIndex < steps.length) { currentStep = newStepIndex; navigationPath.push(currentStep); // Track the path + trackQuestionShown(getCurrentStepNumber()); // Track the new question as shown showStep(currentStep); updateStepper(); } else { @@ -758,6 +807,13 @@ } function endSurvey(endMessage) { + const currentQuestionNumber = getCurrentStepNumber(); + + // Track all remaining questions as skipped + for (let i = currentQuestionNumber + 1; i <= steps.length; i++) { + trackQuestionSkipped(i, `Survey ended early from question ${currentQuestionNumber}`); + } + // Hide all steps and show end message steps.forEach(step => step.style.display = 'none'); @@ -827,7 +883,10 @@ let currentStep = 0; form.addEventListener('submit', function (event) { - console.log('Form submission'); + updateTrackingInputs(); + console.log('Form submission with tracking data'); + console.log('Questions shown:', questionsShownArray); + console.log('Questions skipped:', questionsSkippedArray); const formData = new FormData(form); formData.forEach((value, key) => { console.log(`${key}: ${value}`); @@ -876,6 +935,7 @@ if (currentStep < steps.length - 1) { currentStep++; navigationPath.push(currentStep); // Track normal navigation + trackQuestionShown(getCurrentStepNumber()); // Track question as shown showStep(currentStep); updateStepper(); console.log(`Normal next to question ${currentStep + 1}, path:`, navigationPath); @@ -971,4 +1031,4 @@ }); } -} +} \ No newline at end of file