user response for all the survey completed
This commit is contained in:
parent
1254366a65
commit
9c560a798c
28 changed files with 1261 additions and 40 deletions
48
Services/Implemnetation/DashboardRepository.cs
Normal file
48
Services/Implemnetation/DashboardRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Services/Implemnetation/UserResponseRepository.cs
Normal file
33
Services/Implemnetation/UserResponseRepository.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Services/Interaces/IDashboardRepository.cs
Normal file
15
Services/Interaces/IDashboardRepository.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
13
Services/Interaces/IUserResponseRepository.cs
Normal file
13
Services/Interaces/IUserResponseRepository.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using Model;
|
||||
|
||||
|
||||
|
||||
|
||||
namespace Services.Interaces
|
||||
{
|
||||
public interface IUserResponseRepository
|
||||
{
|
||||
Task<IEnumerable<Response>> GetResponsesByUserAsync(string userName);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
|
|
@ -55,7 +66,15 @@ namespace Web.Areas.Admin.Controllers
|
|||
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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
100
Web/Areas/Admin/Controllers/UserResponseStatusController.cs
Normal file
100
Web/Areas/Admin/Controllers/UserResponseStatusController.cs
Normal 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);
|
||||
//}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
||||
|
||||
62
Web/Areas/Admin/Views/UserResponseStatus/Index.cshtml
Normal file
62
Web/Areas/Admin/Views/UserResponseStatus/Index.cshtml
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
}
|
||||
} *@
|
||||
80
Web/Areas/Admin/Views/newsletters/UploadSubscribers.cshtml
Normal file
80
Web/Areas/Admin/Views/newsletters/UploadSubscribers.cshtml
Normal 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>
|
||||
}
|
||||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
9
Web/ViewModel/DashboardVM/DashboardViewModel.cs
Normal file
9
Web/ViewModel/DashboardVM/DashboardViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
11
Web/ViewModel/NewsLetterVM/PaginationViewModel.cs
Normal file
11
Web/ViewModel/NewsLetterVM/PaginationViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
9
Web/ViewModel/NewsLetterVM/PdfUploadViewModel.cs
Normal file
9
Web/ViewModel/NewsLetterVM/PdfUploadViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
13
Web/ViewModel/QuestionnaireVM/UserResponsesViewModel.cs
Normal file
13
Web/ViewModel/QuestionnaireVM/UserResponsesViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
},
|
||||
"MailJet": {
|
||||
"ApiKey": "f545eee3a4743464b9d25fb9c5ab3f6c",
|
||||
"SecretKey": "9fa430ef00873fdefe333fdc40ee3f8f"
|
||||
"SecretKey": "8df3cf0337a090b1d6301f312ca51413"
|
||||
},
|
||||
"OpenAI": {
|
||||
"ApiKey": "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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue