user response for all the survey completed

This commit is contained in:
Qais Yousuf 2024-06-01 18:38:06 +02:00
parent 1254366a65
commit 9c560a798c
28 changed files with 1261 additions and 40 deletions

View file

@ -0,0 +1,48 @@
using Data;
using Microsoft.EntityFrameworkCore;
using Services.Interaces;
namespace Services.Implemnetation
{
public class DashboardRepository : IDashboardRepository
{
private readonly SurveyContext _context;
public DashboardRepository(SurveyContext Context)
{
_context = Context;
}
public async Task<Dictionary<string, int>> GetModelCountsAsync()
{
var counts = new Dictionary<string, int>
{
{ "Pages", await _context.Pages.CountAsync() },
{ "Banners", await _context.Banners.CountAsync() },
{ "Addresses", await _context.Addresss.CountAsync() },
{ "Footers", await _context.Footers.CountAsync() },
{ "SocialMedia", await _context.SocialMedia.CountAsync() },
{ "FooterSocialMedias", await _context.FooterSocialMedias.CountAsync() },
{ "Subscriptions", await _context.Subscriptions.CountAsync() },
{ "SentNewsletterEmails", await _context.SentNewsletterEamils.CountAsync() }
};
return counts;
}
public async Task<Dictionary<string, int>> GetCurrentBannerSelectionsAsync()
{
return await _context.Pages
.GroupBy(p => p.banner.Title)
.Select(g => new { BannerId = g.Key, Count = g.Count() })
.ToDictionaryAsync(g => g.BannerId.ToString(), g => g.Count);
}
public async Task<Dictionary<string, int>> GetCurrentFooterSelectionsAsync()
{
return await _context.Pages
.GroupBy(p => p.footer.Title)
.Select(g => new { FooterId = g.Key, Count = g.Count() })
.ToDictionaryAsync(g => g.FooterId.ToString(), g => g.Count);
}
}
}

View file

@ -0,0 +1,33 @@
using Data;
using Microsoft.EntityFrameworkCore;
using Model;
using Services.Interaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Services.Implemnetation
{
public class UserResponseRepository : IUserResponseRepository
{
private readonly SurveyContext _context;
public UserResponseRepository(SurveyContext context)
{
_context = context;
}
public async Task<IEnumerable<Response>> GetResponsesByUserAsync(string userName)
{
return await _context.Responses
.Include(r => r.Questionnaire)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.Question)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.Where(r => r.UserName == userName)
.ToListAsync();
}
}
}

View file

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Services.Interaces
{
public interface IDashboardRepository
{
Task<Dictionary<string, int>> GetModelCountsAsync();
Task<Dictionary<string, int>> GetCurrentBannerSelectionsAsync();
Task<Dictionary<string, int>> GetCurrentFooterSelectionsAsync();
}
}

View file

@ -0,0 +1,13 @@
using Model;
namespace Services.Interaces
{
public interface IUserResponseRepository
{
Task<IEnumerable<Response>> GetResponsesByUserAsync(string userName);
}
}

View file

@ -2,6 +2,8 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Model;
using Services.Interaces;
using Web.ViewModel.DashboardVM;
namespace Web.Areas.Admin.Controllers
{
@ -11,14 +13,27 @@ namespace Web.Areas.Admin.Controllers
public class AdminController : Controller
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IDashboardRepository _dashboard;
public AdminController(SignInManager<ApplicationUser> signInManager)
public AdminController(SignInManager<ApplicationUser> signInManager,IDashboardRepository dashboard)
{
_signInManager = signInManager;
_dashboard = dashboard;
}
public IActionResult Index()
public async Task<IActionResult> Index()
{
return View();
var modelCounts = await _dashboard.GetModelCountsAsync();
var bannerSelections = await _dashboard.GetCurrentBannerSelectionsAsync();
var footerSelections = await _dashboard.GetCurrentFooterSelectionsAsync();
var viewModel = new DashboardViewModel
{
ModelCounts = modelCounts,
BannerSelections = bannerSelections,
FooterSelections = footerSelections
};
return View(viewModel);
}
[HttpPost]

View file

@ -14,6 +14,9 @@ using Services.Implemnetation;
using Services.Interaces;
using Web.AIConfiguration;
using Web.ViewModel.NewsLetterVM;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas.Parser;
using System.Text.RegularExpressions;
namespace Web.Areas.Admin.Controllers
{
@ -35,13 +38,21 @@ namespace Web.Areas.Admin.Controllers
_configuration = configuration;
}
public IActionResult Index()
public IActionResult Index(int page = 1)
{
var totalSubscribedUsers = _context.Subscriptions.Count(s => s.IsSubscribed);
const int PageSize = 10;
// Pass the total count to the view
var totalSubscribedUsers = _context.Subscriptions.Count(s => s.IsSubscribed);
ViewBag.TotalSubscribedUsers = totalSubscribedUsers;
var newsLetterFromdb = _repository.GetAll();
var totalSubscriptions = _context.Subscriptions.Count();
var totalPages = (int)Math.Ceiling(totalSubscriptions / (double)PageSize);
var newsLetterFromdb = _repository.GetAll()
.OrderByDescending(x=>x.Id)
.Skip((page - 1) * PageSize)
.Take(PageSize)
.ToList();
var viewmodel = new List<NewsLetterViewModel>();
@ -49,13 +60,21 @@ namespace Web.Areas.Admin.Controllers
{
viewmodel.Add(new NewsLetterViewModel
{
Id=item.Id,
Name=item.Name,
Email=item.Email,
IsSubscribed=item.IsSubscribed
Id = item.Id,
Name = item.Name,
Email = item.Email,
IsSubscribed = item.IsSubscribed
});
}
return View(viewmodel);
var listViewModel = new PaginationViewModel
{
Subscriptions = viewmodel,
CurrentPage = page,
TotalPages = totalPages
};
return View(listViewModel);
}
public IActionResult Create()
@ -70,7 +89,21 @@ namespace Web.Areas.Admin.Controllers
[HttpPost]
public IActionResult DeleteSelectedSubscription(List<int> selectedIds)
{
if (selectedIds != null && selectedIds.Any())
{
var subscriptions = _context.Subscriptions.Where(s => selectedIds.Contains(s.Id)).ToList();
_context.Subscriptions.RemoveRange(subscriptions);
_context.SaveChanges();
}
TempData["Success"] = "Subscriber deleted successfully";
return RedirectToAction(nameof(Index));
}
[HttpPost]
public async Task<IActionResult> Create(SendNewsLetterViewModel viewModel)
{
if (ModelState.IsValid)
@ -183,10 +216,77 @@ namespace Web.Areas.Admin.Controllers
return View(viewModel);
}
[HttpGet]
public IActionResult UploadSubscribers()
{
return View();
}
[HttpPost]
public async Task<IActionResult> UploadSubscribers(PdfUploadViewModel viewModel)
{
if (!ModelState.IsValid)
{
TempData["error"] = "Invalid model state.";
return BadRequest("Invalid model state.");
}
if (viewModel.SubscriberFile == null || viewModel.SubscriberFile.Length == 0)
{
TempData["error"] = "No file uploaded or file is empty.";
return BadRequest("No file uploaded or file is empty.");
}
var newSubscribers = new List<Subscription>();
try
{
using (var pdfReader = new PdfReader(viewModel.SubscriberFile.OpenReadStream()))
using (var pdfDocument = new PdfDocument(pdfReader))
{
for (int page = 1; page <= pdfDocument.GetNumberOfPages(); page++)
{
var text = PdfTextExtractor.GetTextFromPage(pdfDocument.GetPage(page));
var matches = Regex.Matches(text, @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", RegexOptions.IgnoreCase);
foreach (Match match in matches)
{
var email = match.Value.ToLower();
var name = email.Split('@')[0].Replace(".", " ").Replace("_", " ");
if (!newSubscribers.Any(s => s.Email == email))
{
newSubscribers.Add(new Subscription { Email = email, Name = name, IsSubscribed = true });
}
}
}
}
// Optional: Check existing emails to avoid duplicates
var existingEmails = _context.Subscriptions.Select(s => s.Email).ToHashSet();
newSubscribers = newSubscribers.Where(s => !existingEmails.Contains(s.Email)).ToList();
if (newSubscribers.Any())
{
_context.Subscriptions.AddRange(newSubscribers);
await _context.SaveChangesAsync();
TempData["success"] = $"{newSubscribers.Count} new subscribers added successfully.";
return Ok($"{newSubscribers.Count} new subscribers added successfully.");
}
else
{
TempData["info"] = "No new subscribers found in the file.";
return Ok("No new subscribers found in the file.");
}
}
catch (Exception ex)
{
TempData["error"] = $"Error processing file: {ex.Message}";
return BadRequest($"Error processing file: {ex.Message}");
}
}
[HttpPost]
public async Task<IActionResult> MailjetWebhook()
public async Task<IActionResult> MailTracking()
{
using (var reader = new StreamReader(Request.Body))
{

View file

@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Model;
using Services.Interaces;
using Web.ViewModel.QuestionnaireVM;
namespace Web.Areas.Admin.Controllers
{
@ -11,10 +13,12 @@ namespace Web.Areas.Admin.Controllers
public class UserResponseController : Controller
{
private readonly SurveyContext _context;
private readonly IUserResponseRepository _userResponse;
public UserResponseController(SurveyContext context)
public UserResponseController(SurveyContext context, IUserResponseRepository userResponse)
{
_context = context;
_userResponse = userResponse;
}
public async Task<IActionResult> Index()
{
@ -51,6 +55,31 @@ namespace Web.Areas.Admin.Controllers
}
public async Task<IActionResult> UserResponsesStatus(string userName)
{
var responses = await _userResponse.GetResponsesByUserAsync(userName);
if (responses == null || !responses.Any())
{
return NotFound();
}
var userEmail = responses.First().UserEmail;
var viewModel = new UserResponsesViewModel
{
UserName = userName,
UserEmail = userEmail,
Responses = responses.ToList()
};
return View(viewModel);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)

View file

@ -0,0 +1,100 @@
using Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Model;
using Services.Interaces;
using Web.ViewModel.QuestionnaireVM;
namespace Web.Areas.Admin.Controllers
{
public class UserResponseStatusController : Controller
{
private readonly SurveyContext _context;
private readonly IUserResponseRepository _userResponse;
public UserResponseStatusController(SurveyContext context,IUserResponseRepository userResponse)
{
_context = context;
_userResponse = userResponse;
}
public async Task<IActionResult> Index()
{
var usersWithQuestionnaires = await _context.Responses
.Include(r => r.Questionnaire)
.GroupBy(r => r.UserEmail)
.Select(g => new UserResponsesViewModel
{
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();
return View(usersWithQuestionnaires);
}
public async Task<IActionResult> UserResponsesStatus(string userEmail)
{
var responses = await _context.Responses
.Include(r => r.Questionnaire)
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.Question)
.ThenInclude(q => q.Answers) // Include the Answers entity
.Include(r => r.ResponseDetails)
.ThenInclude(rd => rd.ResponseAnswers)
.Where(r => r.UserEmail == userEmail)
.ToListAsync();
if (responses == null || !responses.Any())
{
return NotFound();
}
var userName = responses.First().UserName;
var viewModel = new UserResponsesViewModel
{
UserName = userName,
UserEmail = userEmail,
Responses = responses
};
return View(viewModel);
}
//public async Task<IActionResult> UserResponsesStatus(string userEmail)
//{
// var responses = await _context.Responses
// .Include(r => r.Questionnaire)
// .Include(r => r.ResponseDetails)
// .ThenInclude(rd => rd.Question)
// .Include(r => r.ResponseDetails)
// .ThenInclude(rd => rd.ResponseAnswers)
// .Where(r => r.UserEmail == userEmail)
// .ToListAsync();
// if (responses == null || !responses.Any())
// {
// return NotFound();
// }
// var userName = responses.First().UserName;
// var viewModel = new UserResponsesViewModel
// {
// UserName = userName,
// UserEmail = userEmail,
// Responses = responses
// };
// return View(viewModel);
//}
}
}

View file

@ -1 +1,68 @@

@model DashboardViewModel
<h2>Admin Dashboard</h2>
<div id="modelCountChart" style="width: 100%; height: 500px;"></div>
<div id="bannerSelectionChart" style="width: 100%; height: 500px;"></div>
<div id="footerSelectionChart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
google.charts.load('current', { 'packages': ['corechart'] });
google.charts.setOnLoadCallback(drawModelCountChart);
google.charts.setOnLoadCallback(drawBannerSelectionChart);
google.charts.setOnLoadCallback(drawFooterSelectionChart);
function drawModelCountChart() {
var data = google.visualization.arrayToDataTable([
['Model', 'Count'],
@foreach (var entry in Model.ModelCounts)
{
<text>['@entry.Key', @entry.Value], </text>
}
]);
var options = {
title: 'Model Counts',
};
var chart = new google.visualization.PieChart(document.getElementById('modelCountChart'));
chart.draw(data, options);
}
function drawBannerSelectionChart() {
var data = google.visualization.arrayToDataTable([
['Banner ID', 'Count'],
@foreach (var entry in Model.BannerSelections)
{
<text>['@entry.Key', @entry.Value], </text>
}
]);
var options = {
title: 'Banner Selections',
};
var chart = new google.visualization.PieChart(document.getElementById('bannerSelectionChart'));
chart.draw(data, options);
}
function drawFooterSelectionChart() {
var data = google.visualization.arrayToDataTable([
['Footer ID', 'Count'],
@foreach (var entry in Model.FooterSelections)
{
<text>['@entry.Key', @entry.Value], </text>
}
]);
var options = {
title: 'Footer Selections',
};
var chart = new google.visualization.PieChart(document.getElementById('footerSelectionChart'));
chart.draw(data, options);
}
</script>

View file

@ -46,6 +46,10 @@
<a asp-controller="UserResponse" asp-action="index"><span class="bi bi-clipboard-data"></span> Response</a>
</li>
<li>
<a asp-controller="UserResponseStatus" asp-action="index"><span class="bi bi-heart-pulse"></span> User status</a>
</li>
<li>
<a asp-controller="newsletters" asp-action="index"><span class="bi bi-newspaper"></span> Subscibers</a>
</li>

View file

@ -1,4 +1,4 @@
@if (TempData["Success"] != null)
@if (TempData["success"] != null)
{
<script src="/lib/jquery/dist/jquery.min.js"></script>
@ -6,10 +6,21 @@
<script type="text/javascript">
toastr.success('@TempData["success"]')
</script>
}
@if (TempData["error"] != null)
{
<script src="/lib/jquery/dist/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js"></script>
<script type="text/javascript">
toastr.info('@TempData["Error"]')
</script>
}

View file

@ -0,0 +1,43 @@
@model UserResponsesViewModel
@{
ViewData["Title"] = "User Responses";
}
<h2>User Responses</h2>
<div>
<h3>@Model.UserName (@Model.UserEmail)</h3>
</div>
@foreach (var response in Model.Responses)
{
<div>
<h4>Questionnaire: @response.Questionnaire.Title</h4>
<p>Submitted on: @response.SubmissionDate</p>
<ul>
@foreach (var detail in response.ResponseDetails)
{
<li>
Question: @detail.Question.Text
@if (detail.QuestionType == QuestionType.Text)
{
<p>Answer: @detail.TextResponse</p>
}
else
{
<ul>
@foreach (var answer in detail.ResponseAnswers)
{
<li>Answer ID: @answer.AnswerId</li>
}
</ul>
}
</li>
}
</ul>
</div>
}

View file

@ -0,0 +1,62 @@
@model IEnumerable<UserResponsesViewModel>
@{
ViewData["Title"] = "User Responses status";
}
<div class="container-fluid mt-4 mb-5">
<div class="col-10 offset-1 ">
<div class="card p-4 shadow-lg rounded-2">
<h3 class="text-primary">Response status</h3>
<form asp-action="DeleteSelected" method="post">
<table class="table table-responsive w-100 d-block d-md-table table-bordered table-hover">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Survey</th>
<th>Action</th>
<!-- Additional headers omitted for brevity -->
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>@item.UserName</td>
<td>@item.UserEmail</td>
<td>
<ul>
@foreach (var response in item.Responses)
{
<span class="badge badge-primary p-2 shadow">@response.Questionnaire.Title</span>
}
</ul>
</td>
<td class="text-end">
<a asp-controller="UserResponseStatus" asp-action="UserResponsesStatus" asp-route-UserEmail="@item.UserEmail" class="btn btn-info btn-sm"><i class="bi bi-eye"></i> View Responses status</a>
</td>
</tr>
}
</tbody>
</table>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,292 @@
@model UserResponsesViewModel
@{
ViewData["Title"] = "User Responses";
}
<style>
.stepper-wrapper {
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
}
.stepper-item {
display: flex;
align-items: flex-start;
position: relative;
padding-left: 120px;
}
.stepper-item::before {
content: '';
position: absolute;
left: 20px;
top: 0;
bottom: 0;
width: 2px;
background-color: #007bff;
}
.step-counter {
position: absolute;
left: 0;
top: 0;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background-color: #007bff;
color: white;
border-radius: 50%;
font-size: 14px;
font-weight: bold;
}
.step-content {
flex-grow: 1;
}
.step-header {
margin-bottom: 10px;
}
</style>
<div class="container-fluid mt-3">
<p>
<a asp-action="Index" class="btn btn-primary btn-sm">Back to list</a>
</p>
<div class="card p-5 m-3 shadow">
<div class="bd-callout bd-callout-primary">
<h4 class="text-primary">User Responses</h4>
<text>@Model.UserName (@Model.UserEmail)</text>
</div>
<!-- Stepper -->
<div class="stepper-wrapper">
@foreach (var response in Model.Responses)
{
<div class="stepper-item">
<div class="step-counter">
<span class="badge bg-primary p-3 shadow">@response.Questionnaire.Title</span>
</div>
<div class="step-content">
<div class="card p-4">
<div class="step-header">
<h6>Survey: @response.Questionnaire.Title</h6>
<p>Submitted on: @response.SubmissionDate</p>
</div>
<!-- Collapsible button -->
<button class="btn btn-primary btn-sm mt-2" type="button" data-bs-toggle="collapse" data-bs-target="#collapseResponse-@response.Id" aria-expanded="false" aria-controls="collapseResponse-@response.Id">
View Responses
</button>
<!-- Collapsible content -->
<div class="collapse mt-3" id="collapseResponse-@response.Id">
<table class="table table-responsive w-100 d-block d-md-table table-hover">
<thead>
<tr>
<th>Question</th>
<th>Response</th>
</tr>
</thead>
<tbody>
@foreach (var detail in response.ResponseDetails)
{
<tr>
<td>@detail.Question.Text</td>
<td>
@if (detail.QuestionType == QuestionType.Text || detail.QuestionType == QuestionType.Slider || detail.QuestionType == QuestionType.Open_ended)
{
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center border-1">
Question type
<span class="badge text-bg-primary rounded-pill p-1">@detail.QuestionType</span>
</li>
</ul>
<br />
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center border-1">
Answer
<span class="badge text-bg-primary rounded-pill p-1">@detail.TextResponse</span>
</li>
</ul>
}
else
{
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center border-1">
Question type
<span class="badge text-bg-primary rounded-pill p-1">@detail.QuestionType</span>
</li>
</ul>
<br />
<ul class="list-group">
@foreach (var answer in detail.ResponseAnswers)
{
<li class="list-group-item d-flex justify-content-between align-items-center">
Answer
<span class="badge text-bg-primary rounded-pill p-1">@detail.Question.Answers.FirstOrDefault(a => a.Id == answer.AnswerId)?.Text</span>
</li>
}
</ul>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
}
</div>
</div>
</div>
@* <div class="container-fluid mt-3">
<p>
<a asp-action="Index" class="btn btn-primary btn-sm">Back to list</a>
</p>
<div class="card p-5 m-3 shadow">
<div class="bd-callout bd-callout-primary">
<h4 class="text-primary">User Responses</h4>
<text>@Model.UserName (@Model.UserEmail)</text>
</div>
<div>
@foreach (var response in Model.Responses)
{
<div class="container card mt-4 p-3">
<div>
<h6>Survey: @response.Questionnaire.Title</h6>
<p>Submitted on: @response.SubmissionDate</p>
<!-- Collapsible button -->
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#collapseResponse-@response.Id" aria-expanded="false" aria-controls="collapseResponse-@response.Id">
View Responses
</button>
<!-- Collapsible content -->
<div class="collapse mt-3" id="collapseResponse-@response.Id">
<table class="table table-responsive w-100 d-block d-md-table table-hover">
<thead>
<tr>
<th>Question</th>
<th>Response</th>
</tr>
</thead>
<tbody>
@foreach (var detail in response.ResponseDetails)
{
<tr>
<td>@detail.Question.Text</td>
<td>
@if (detail.QuestionType == QuestionType.Text || detail.QuestionType == QuestionType.Slider || detail.QuestionType == QuestionType.Open_ended)
{
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center border-1">
Question type
<span class="badge text-bg-primary rounded-pill p-1s">@detail.QuestionType</span>
</li>
</ul>
<br />
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center border-1">
Answer
<span class="badge text-bg-primary rounded-pill p-1">@detail.TextResponse</span>
</li>
</ul>
}
else
{
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center border-1">
Question type
<span class="badge text-bg-primary rounded-pill p-1">@detail.QuestionType</span>
</li>
</ul>
<br />
<ul class="list-group">
@foreach (var answer in detail.ResponseAnswers)
{
<li class="list-group-item d-flex justify-content-between align-items-center">
Answer
<span class="badge text-bg-primary rounded-pill p-1">@detail.Question.Answers.FirstOrDefault(a => a.Id == answer.AnswerId)?.Text</span>
</li>
}
</ul>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
</div>
</div>
</div> *@
@section Scripts {
<!-- Include Bootstrap 5 JS for collapse functionality -->
<script>
document.addEventListener('DOMContentLoaded', function () {
var collapseElements = document.querySelectorAll('[data-bs-toggle="collapse"]');
collapseElements.forEach(function (element) {
var targetId = element.getAttribute('data-bs-target');
var targetElement = document.querySelector(targetId);
// Initialize button text and color based on current state
if (targetElement.classList.contains('show')) {
element.textContent = 'Hide Responses';
element.classList.remove('btn-primary');
element.classList.add('btn-info');
} else {
element.textContent = 'View Responses';
element.classList.remove('btn-info');
element.classList.add('btn-primary');
}
targetElement.addEventListener('shown.bs.collapse', function () {
element.textContent = 'Hide Responses';
element.classList.remove('btn-primary');
element.classList.add('btn-info');
});
targetElement.addEventListener('hidden.bs.collapse', function () {
element.textContent = 'View Responses';
element.classList.remove('btn-info');
element.classList.add('btn-primary');
});
});
});
</script>
}

View file

@ -10,6 +10,7 @@
@using Web.ViewModel.PageVM
@using Web.ViewModel.AccountVM
@using Web.ViewModel.NewsLetterVM
@using Web.ViewModel.DashboardVM
@using Services.EmailSend
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View file

@ -1,8 +1,10 @@
@model IEnumerable<Web.ViewModel.NewsLetterVM.NewsLetterViewModel>
@model PaginationViewModel
@{
ViewData["Title"] = "Newsletter list";
}
<style>
.badge-Sent {
display: inline-block;
@ -121,6 +123,169 @@
<partial name="_Notification" />
<div class="card bg-default mb-3 rounded-2 shadow-lg">
<div class="card-header">Subscribers</div>
<div class="card-body">
<h4 class="card-title">Subscribers list</h4>
<div class="alert alert-info" role="alert">
Total Subscribed Users: <strong>@ViewBag.TotalSubscribedUsers</strong>
</div>
<p>
<a asp-action="UploadSubscribers" class="btn btn-info"><i class="bi bi-cloud-arrow-up"></i> Upload subscribers form file</a>
</p>
<p>
<a asp-action="Create" class="btn btn-primary btn-sm @(@ViewBag.TotalSubscribedUsers <= 0 ? "disabled" : "")">compose newsletter</a>
</p>
<form id="deleteForm" method="post" asp-action="DeleteSelectedSubscription">
<table class="table table-responsive w-100 d-block d-md-table">
<thead>
<tr>
<th scope="col">
<input type="checkbox" id="selectAll">
</th>
<th scope="col">Id</th>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">IsSubscribed</th>
<th scope="col" class="d-flex justify-content-end">Action</th>
</tr>
</thead>
<tbody class="justify-content-center">
@foreach (var item in Model.Subscriptions)
{
<tr class="table-secondary">
<td>
<input type="checkbox" name="selectedIds" value="@item.Id">
</td>
<td>@item.Id</td>
<td>@item.Name</td>
<td>@item.Email</td>
<td>
@if(item.IsSubscribed)
{
<span class="badge badge-success p-1">Subscribed @Html.DisplayFor(modelItem => item.IsSubscribed)</span>
}
else
{
<span class="badge badge-secondary p-1">Not subscribed @Html.DisplayFor(modelItem => item.IsSubscribed)</span>
}
</td>
<td class="d-flex justify-content-end">
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-danger btn-sm"><i class="bi bi-trash"></i> Delete</a>
</td>
</tr>
}
</tbody>
</table>
<button type="submit" class="btn btn-danger mt-3">Delete Selected</button>
</form>
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
@for (int i = 1; i <= Model.TotalPages; i++)
{
<li class="page-item @(i == Model.CurrentPage ? "active" : "")">
<a class="page-link" asp-action="Index" asp-route-page="@i">@i</a>
</li>
}
</ul>
</nav>
</div>
</div>
</div>
<div class="container-fluid mb-5 mt-4">
<div class="col-md-10 col-lg-10 col-sm-12 offset-1">
<div class="card rounded-2 shadow-lg p-3 mt-3">
<h4 class="text-primary">
<i class="bi bi-broadcast"></i> Real-Time Email Tracking
</h4>
<p>
<a asp-action="EmailStats" class="btn btn-primary btn-sm">View email tracking with chart</a>
</p>
<table class="table table-responsive d-block d-md-table table-bordered table-hover mt-3">
<thead>
<tr>
<th>Recipient</th>
<th>Activity Date</th>
<th>Subject</th>
<th>IP</th>
<th>Country</th>
<th>Sent</th>
<th>Delivered</th>
<th>Opened</th>
<th>Clicked</th>
<th>Bounced</th>
<th>Spam</th>
<th>Blocked</th>
<th>Unsubscribed</th>
</tr>
</thead>
<tbody id="emailStatsTableBody">
<!-- Rows will be dynamically inserted here -->
</tbody>
</table>
</div>
</div>
</div>
@section Scripts{
<script>
document.addEventListener('DOMContentLoaded', function () {
function fetchData() {
fetch('@Url.Action("GetEmailStatsData", "Newsletters")')
.then(response => response.json())
.then(data => {
updateTable(data);
})
.catch(error => console.error('Error fetching data:', error));
}
function updateTable(data) {
const tableBody = document.getElementById('emailStatsTableBody');
tableBody.innerHTML = ''; // Clear existing table rows
data.forEach(item => {
const row = `
<tr>
<td>${item.recipientEmail}</td>
<td>${item.receivedActivity}</td>
<td>${item.subject}</td>
<td>${item.ipAddress}</td>
<td>${item.geo}</td>
<td>${item.isSent ? '<span class="badge-Sent">Sent</span>' : '<span class="badge badge-secondary">Pending</span>'}</td>
<td>${item.isDelivered ? '<span class="badge-Deliverd">Delivered</span>' : '<span class="badge badge-secondary">Pending</span>'}</td>
<td>${item.isOpened ? '<span class="badge-Opend">Opened</span>' : '<span class="badge badge-secondary">Pending</span>'}</td>
<td>${item.isClicked ? '<span class="badge-Clicked">Clicked</span>' : '<span class="badge badge-secondary">Pending</span>'}</td>
<td>${item.isBounced ? '<span class="badge-Bounced">Bounced</span>' : '<span class="badge badge-secondary">Normal</span>'}</td>
<td>${item.isSpam ? '<span class="badge-Spam">Spamed</span>' : '<span class="badge badge-secondary">Normal</span>'}</td>
<td>${item.isBlocked ? '<span class="badge-Blocked">Blocked</span>' : '<span class="badge badge-secondary">Normal</span>'}</td>
<td>${item.isUnsubscribed ? '<span class="badge-Unsubscribed">Unsubscribed</span>' : '<span class="badge badge-secondary">Normal</span>'}</td>
</tr>
`;
tableBody.innerHTML += row; // Append new row
});
}
setInterval(fetchData, 5000); // Fetch data every 5 seconds
document.getElementById('selectAll').addEventListener('click', function() {
const checkboxes = document.querySelectorAll('input[name="selectedIds"]');
checkboxes.forEach(checkbox => checkbox.checked = this.checked);
});
});
</script>
}
@* <div class="container mt-5 mb-3">
<partial name="_Notification" />
<div class="card bg-default mb-3 rounded-2 shadow-lg">
<div class="card-header">Subscribers</div>
<div class="card-body">
@ -129,14 +294,20 @@
Total Subscribed Users: <strong>@ViewBag.TotalSubscribedUsers</strong>
</div>
<p>
<a asp-action="UploadSubscribers" class="btn btn-info">Upload subscribers form file</a>
</p>
<p>
<a asp-action="Create" class="btn btn-primary btn-sm @(@ViewBag.TotalSubscribedUsers <= 0 ? "disabled" : "")">compose newsletter</a>
</p>
<table class="table table-responsive w-100 d-block d-md-table ">
<form id="deleteForm" method="post" asp-action="DeleteSelectedSubscription">
<table class="table table-responsive w-100 d-block d-md-table ">
<thead>
<tr>
<th scope="col">
<input type="checkbox" id="selectAll">
</th>
<th scope="col">Id</th>
<th scope="col">Name</th>
<th scope="col">Email</th>
@ -148,7 +319,9 @@
@foreach (var item in Model)
{
<tr class=" table-secondary">
<td>
<input type="checkbox" name="selectedIds" value="@item.Id">
</td>
<td>@item.Id</td>
<td>@item.Name</td>
<td>@item.Email</td>
@ -172,6 +345,9 @@
</tbody>
</table>
<button type="submit" class="btn btn-danger mt-3">Delete Selected</button>
</form>
</div>
</div>
@ -192,6 +368,7 @@
<p>
<a asp-action="EmailStats" class="btn btn-primary btn-sm">View email tracking with chart</a>
</p>
<table class="table table-responsive d-block d-md-table table-bordered table-hover mt-3">
<thead>
<tr>
@ -260,7 +437,11 @@
}
setInterval(fetchData, 5000); // Fetch data every 5 seconds
document.getElementById('selectAll').addEventListener('click', function() {
const checkboxes = document.querySelectorAll('input[name="selectedIds"]');
checkboxes.forEach(checkbox => checkbox.checked = this.checked);
});
});
</script>
}
} *@

View file

@ -0,0 +1,80 @@
@model PdfUploadViewModel
@{
ViewData["Title"] = "UploadSubscribers";
}
<style>
.loader {
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
position: absolute;
top: 50%;
left: 50%;
margin-left: -60px;
margin-top: -60px;
z-index: 1000;
}
@@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<partial name="_Notification" />
<div class="container mt-5">
<form id="uploadForm" asp-action="UploadSubscribers" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="SubscriberFile">Upload PDF:</label>
<input type="file" name="SubscriberFile" class="form-control" required />
</div>
<button type="submit" class="btn btn-primary"><i class="bi bi-box-arrow-in-down"></i> Upload</button>
</form>
<div id="loader" class="loader" style="display: none;"></div>
</div>
@section Scripts {
@{
<partial name="_ValidationScriptsPartial" />
}
<script>
document.getElementById('uploadForm').addEventListener('submit', function(event) {
event.preventDefault();
var form = event.target;
var formData = new FormData(form);
var xhr = new XMLHttpRequest();
// Show the loader
document.getElementById('loader').style.display = 'block';
xhr.addEventListener('load', function() {
// Hide the loader
document.getElementById('loader').style.display = 'none';
if (xhr.status === 200) {
window.location.href = '@Url.Action("Index", "Newsletters")';
} else {
alert('Upload failed!');
}
});
xhr.addEventListener('error', function() {
// Hide the loader
document.getElementById('loader').style.display = 'none';
alert('Upload failed!');
});
xhr.open('POST', form.action);
xhr.setRequestHeader("RequestVerificationToken", document.querySelector('input[name="__RequestVerificationToken"]').value);
xhr.send(formData);
});
</script>
}

View file

@ -53,6 +53,10 @@ namespace Web.Extesions
{
services.AddScoped<ISocialMediaRepository,SocialMediaRepository>();
}
public static void ConfigureDashboard(this IServiceCollection services)
{
services.AddScoped<IDashboardRepository, DashboardRepository>();
}
public static void ConfigureFooter(this IServiceCollection services)
{
@ -74,6 +78,10 @@ namespace Web.Extesions
{
services.AddTransient<IEmailServices, EmailServices>();
}
public static void UserResponseConfiguration(this IServiceCollection services)
{
services.AddTransient<IUserResponseRepository, UserResponseRepository>();
}
public static void MailStatConfiguration(this IServiceCollection services)
{
services.AddTransient<IEmailStatsService, EmailStatsService>();

View file

@ -38,6 +38,8 @@ builder.Services.AddTransient<NavigationViewComponent>();
builder.Services.ConfigureNewsLetter();
builder.Services.MailConfiguration();
builder.Services.MailStatConfiguration();
builder.Services.ConfigureDashboard();
builder.Services.UserResponseConfiguration();
builder.Services.ConfigureOpenAI(config);

View file

@ -9,15 +9,7 @@
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5205",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,

View file

@ -0,0 +1,9 @@
namespace Web.ViewModel.DashboardVM
{
public class DashboardViewModel
{
public Dictionary<string, int>? ModelCounts { get; set; }
public Dictionary<string, int>? BannerSelections { get; set; }
public Dictionary<string, int>? FooterSelections { get; set; }
}
}

View file

@ -0,0 +1,11 @@
using Model;
namespace Web.ViewModel.NewsLetterVM
{
public class PaginationViewModel
{
public IEnumerable<NewsLetterViewModel>? Subscriptions { get; set; }
public int CurrentPage { get; set; }
public int TotalPages { get; set; }
}
}

View file

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace Web.ViewModel.NewsLetterVM
{
public class PdfUploadViewModel
{
[Required(ErrorMessage = "Please upload a file.")]
public IFormFile SubscriberFile { get; set; }
}
}

View file

@ -0,0 +1,13 @@

using Model;
namespace Web.ViewModel.QuestionnaireVM
{
public class UserResponsesViewModel
{
public string? UserName { get; set; }
public string? UserEmail { get; set; }
public List<Response>? Responses { get; set; }
}
}

View file

@ -320,15 +320,23 @@
<h4>@Model.Title</h4>
<p>@Html.Raw(Model.Description)</p>
<div class="form-group">
<label for="userName">Your Name:</label>
<div class="container">
<div class="continaer">
<div class="row">
<div class="col-md-6 col-lg-6 col-sm-12">
<div class="mb-3">
<label for="userName">Your Name</label>
<input type="text" class="form-control" id="userName" name="UserName" placeholder="Enter your name">
</div>
<div class="form-group">
<label for="Email">Email Address:</label>
<div class="mb-3">
<label for="Email">Email Address</label>
<input type="email" class="form-control" id="Email" name="Email" placeholder="Enter your email">
</div>
<div class="container">
</div>
</div>
</div>
<div class="row align-items-center">
<!-- Stepper -->
<div class="col-md-3">

View file

@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="itext7" Version="8.0.4" />
<PackageReference Include="MailJet.Api" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.3" />

View file

@ -21,7 +21,7 @@
},
"MailJet": {
"ApiKey": "f545eee3a4743464b9d25fb9c5ab3f6c",
"SecretKey": "9fa430ef00873fdefe333fdc40ee3f8f"
"SecretKey": "8df3cf0337a090b1d6301f312ca51413"
},
"OpenAI": {
"ApiKey": "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"

View file

@ -41,6 +41,80 @@
.navbar-light .navbar-text{
color:white !important;
}
.bd-callout {
padding: 1.25rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
border: 1px solid #eee;
border-left-width: .25rem;
border-radius: .25rem
}
.bd-callout h4 {
margin-top: 0;
margin-bottom: .25rem
}
.bd-callout p:last-child {
margin-bottom: 0
}
.bd-callout code {
border-radius: .25rem
}
.bd-callout + .bd-callout {
margin-top: -.25rem
}
.bd-callout-info {
border-left-color: #5bc0de
}
.bd-callout-info h4 {
color: #5bc0de
}
.bd-callout-warning {
border-left-color: #f0ad4e
}
.bd-callout-warning h4 {
color: #f0ad4e
}
.bd-callout-danger {
border-left-color: #d9534f
}
.bd-callout-danger h4 {
color: #d9534f
}
.bd-callout-primary {
border-left-color: #007bff
}
.bd-callout-primaryh4 {
color: #007bff
}
.bd-callout-success {
border-left-color: #28a745
}
.bd-callout-successh4 {
color: #28a745
}
.bd-callout-default {
border-left-color: #6c757d
}
.bd-callout-defaulth4 {
color: #6c757d
}
.MainBanner {
display: flex;
flex-wrap: wrap;