Update email services and improve email template in online survey project

Replaced the email service implementation and enhanced the email template design. The main email address for sending online surveys is now survey@asurvey.dk.
This commit is contained in:
Qaisyousuf 2025-08-05 14:22:48 +02:00
parent d06e0c5ba9
commit 285eb3ce93
10 changed files with 535 additions and 96 deletions

View file

@ -8,15 +8,19 @@ namespace Services.EmailSend
{ {
public class EmailToSend public class EmailToSend
{ {
public EmailToSend(string to, string subject, string body) public string To { get; set; }
public string Subject { get; set; }
public string HtmlBody { get; set; }
public Dictionary<string, string> Headers { get; set; }
public EmailToSend(string to, string subject, string htmlBody)
{ {
To = to; To = to;
Subject = subject; Subject = subject;
Body = body; HtmlBody = htmlBody;
Headers = new Dictionary<string, string>(); // optional unless needed
} }
public string To { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
} }
} }

View file

@ -17,37 +17,50 @@ namespace Services.Implemnetation
{ {
_configuration = configuration; _configuration = configuration;
} }
public async Task<bool> SendConfirmationEmailAsync(EmailToSend emailSend) public async Task<bool> SendConfirmationEmailAsync(EmailToSend emailSend)
{ {
var apiKey = _configuration["MailJet:ApiKey"];
var secretKey = _configuration["MailJet:SecretKey"];
var fromEmail = _configuration["Email:From"];
var fromName = _configuration["Email:ApplicationName"];
var client = new MailjetClient(apiKey, secretKey);
var message = new JObject
MailjetClient client = new MailjetClient(_configuration["MailJet:ApiKey"], _configuration["MailJet:SecretKey"]);
var email = new TransactionalEmailBuilder()
.WithFrom(new SendContact(_configuration["Email:From"], _configuration["Email:ApplicationName"]))
.WithSubject(emailSend.Subject)
.WithHtmlPart(emailSend.Body)
.WithTo(new SendContact(emailSend.To))
.Build();
var response = await client.SendTransactionalEmailAsync(email);
if (response.Messages != null)
{ {
if (response.Messages[0].Status == "success") ["From"] = new JObject
{ {
return true; ["Email"] = fromEmail,
["Name"] = fromName
},
["To"] = new JArray
{
new JObject
{
["Email"] = emailSend.To,
["Name"] = emailSend.To.Split('@')[0]
} }
},
["Subject"] = emailSend.Subject,
["HTMLPart"] = emailSend.HtmlBody
};
// ✨ Add headers if any
if (emailSend.Headers != null && emailSend.Headers.Any())
{
message["Headers"] = JObject.FromObject(emailSend.Headers);
} }
var request = new MailjetRequest
{
Resource = SendV31.Resource
}
.Property(Send.Messages, new JArray { message });
return false; var response = await client.PostAsync(request);
return response.IsSuccessStatusCode;
} }
} }
} }

View file

@ -464,80 +464,327 @@ namespace Web.Areas.Admin.Controllers
} }
[HttpPost]
[HttpPost] [HttpPost]
public async Task<IActionResult> SendQuestionnaire(SendQuestionnaireViewModel viewModel) public async Task<IActionResult> SendQuestionnaire(SendQuestionnaireViewModel viewModel)
{ {
if (ModelState.IsValid) if (!ModelState.IsValid)
return View(viewModel);
var questionnairePath = _configuration["Email:Questionnaire"];
var subject = _questionnaire.GetQuesById(viewModel.Id)?.Title ?? "Survey Invitation";
var currentDateTime = viewModel.ExpirationDateTime ?? DateTime.Now;
string token = Guid.NewGuid().ToString();
string tokenWithExpiry = $"{token}|{currentDateTime:yyyy-MM-ddTHH:mm:ssZ}";
var emailList = viewModel.Emails.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(e => e.Trim())
.ToList();
bool allEmailsSent = true;
foreach (var email in emailList)
{ {
var questionnairePath = _configuration["Email:Questionnaire"]; string userName = FormatUserNameFromEmail(email);
DateTime currentDateTime = viewModel.ExpirationDateTime.HasValue ? viewModel.ExpirationDateTime.Value : DateTime.Now; string userEmailEncoded = HttpUtility.UrlEncode(email);
DateTime expiryDateTime = currentDateTime; // This line might need adjustment if you are actually setting an expiry. string completeUrl = $"{Request.Scheme}://{Request.Host}/{questionnairePath}/{viewModel.QuestionnaireId}?t={tokenWithExpiry}&E={userEmailEncoded}";
string token = Guid.NewGuid().ToString();
string tokenWithExpiry = $"{token}|{expiryDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ")}";
var emailList = viewModel.Emails.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(email => email.Trim())
.ToList();
var question = _questionnaire.GetQuesById(viewModel.Id);
var subject = question.Title;
bool allEmailsSent = true; string emailBody = GenerateEmailBody(userName, subject, completeUrl, currentDateTime);
foreach (var email in emailList)
var emailSend = new EmailToSend(email, subject, emailBody)
{ {
var userName = email.Substring(0, email.IndexOf('@')); // This assumes the email is valid and contains an '@' Headers = new Dictionary<string, string>
userName = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(userName.Replace(".", " ")); // Optional: Improve formatting, replace dots and capitalize names {
var userEmailEncoded = HttpUtility.UrlEncode(email); { "X-Priority", "1" },
var completeUrl = $"{Request.Scheme}://{Request.Host}/{questionnairePath}/{viewModel.QuestionnaireId}?t={tokenWithExpiry}&E={userEmailEncoded}"; { "Importance", "High" }
}
};
string emailBody = $@" bool emailSent = await _emailServices.SendConfirmationEmailAsync(emailSend);
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; border: 0.5px solid #ccc; border-radius: 5px; background-color: #f9f9f9; }}
.button {{ display: inline-block; padding: 10px 20px; background-color: #007bff; color: #ffffff; text-decoration: none; border-radius: 4px; }}
.button:hover {{ background-color: #0056b3; }}
</style>
</head>
<body>
<div class='container'>
<h4>Hey {userName},</h4>
<h5>{subject}</h5>
<p>Thank you for participating in our survey. Your feedback is valuable to us.</p>
<p>Please click the button below to start the survey:</p>
<p class='text-danger'>The survey will expire: {expiryDateTime.ToLongDateString()} Time: {expiryDateTime.ToShortTimeString()}</p>
<div style='text-align: center;'>
<a href='{completeUrl}' class='button'>Start Survey</a>
</div><br>
<p><strong>Søren Eggert Lundsteen Olsen</strong><br>
Seosoft ApS<br>
<hr>
Hovedgaden 3 Jordrup<br>
Kolding 6064<br>
Denmark</p>
</div>
</body>
</html>";
var emailSend = new EmailToSend(email, subject, emailBody); if (!emailSent)
bool emailSent = await _emailServices.SendConfirmationEmailAsync(emailSend);
if (!emailSent)
{
allEmailsSent = false;
ModelState.AddModelError(string.Empty, "Failed to send questionnaire via email to: " + email);
}
}
if (allEmailsSent)
{ {
TempData["Success"] = "Questionnaire sent successfully to all recipients."; allEmailsSent = false;
return RedirectToAction(nameof(Index)); ModelState.AddModelError(string.Empty, $"Failed to send questionnaire to: {email}");
} }
} }
// If model state is not valid, return the view with validation errors if (allEmailsSent)
{
TempData["Success"] = "Questionnaire sent successfully to all recipients.";
return RedirectToAction(nameof(Index));
}
return View(viewModel); return View(viewModel);
} }
private string FormatUserNameFromEmail(string email)
{
var usernamePart = email.Split('@')[0];
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(usernamePart.Replace('.', ' '));
}
private static string GenerateEmailBody(string userName, string subject, string url, DateTime expiry)
{
var danishCulture = new CultureInfo("da-DK");
string expiryDate = expiry.ToString("dd. MMMM yyyy", danishCulture);
string expiryTime = expiry.ToString("HH:mm", danishCulture);
return $@"
<!DOCTYPE html>
<html lang='da'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Invitation til undersøgelse</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px 0;
margin: 0;
line-height: 1.6;
color: #333;
}}
.email-wrapper {{
max-width: 650px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}}
.header {{
background: linear-gradient(135deg, #33b3ae 0%, #141c27 100%);
color: white;
padding: 30px;
text-align: center;
}}
.logo {{
max-width: 200px;
height: auto;
margin-bottom: 15px;
filter: brightness(0) invert(1); /* Makes logo white on dark background */
}}
.header h1 {{
font-size: 28px;
font-weight: 600;
margin: 0;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
}}
.header p {{
margin: 8px 0 0 0;
opacity: 0.9;
font-size: 16px;
}}
.content {{
padding: 40px 30px;
}}
.greeting {{
font-size: 20px;
color: #141c27;
margin-bottom: 20px;
font-weight: 500;
}}
.subject-line {{
background: linear-gradient(135deg, #33b3ae10 0%, #33b3ae20 100%);
border-left: 4px solid #33b3ae;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
font-weight: 600;
font-size: 18px;
color: #141c27;
}}
.content p {{
margin: 16px 0;
font-size: 16px;
color: #555;
line-height: 1.7;
}}
.cta-section {{
text-align: center;
margin: 35px 0;
padding: 20px;
}}
.cta-button {{
display: inline-block;
background: linear-gradient(135deg, #33b3ae 0%, #2a9d99 100%);
color: white;
text-decoration: none;
padding: 16px 32px;
border-radius: 50px;
font-weight: 600;
font-size: 16px;
box-shadow: 0 8px 25px rgba(51, 179, 174, 0.3);
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}}
.cta-button:hover {{
transform: translateY(-2px);
box-shadow: 0 12px 35px rgba(51, 179, 174, 0.4);
text-decoration: none;
color: white;
}}
.expiry-notice {{
background: linear-gradient(135deg, #fff5f5 0%, #fed7d7 30%);
border: 1px solid #feb2b2;
border-radius: 8px;
padding: 20px;
margin: 25px 0;
text-align: center;
}}
.expiry-notice .icon {{
font-size: 24px;
margin-bottom: 8px;
display: block;
}}
.expiry-notice p {{
margin: 0;
color: #c53030;
font-weight: 600;
font-size: 16px;
}}
.footer {{
background-color: #f8f9fb;
padding: 30px;
border-top: 1px solid #e2e8f0;
}}
.company-info {{
text-align: center;
color: #666;
font-size: 14px;
line-height: 1.6;
}}
.company-info h3 {{
color: #141c27;
margin-bottom: 15px;
font-size: 18px;
font-weight: 600;
}}
.company-info a {{
color: #33b3ae;
text-decoration: none;
font-weight: 500;
}}
.company-info a:hover {{
text-decoration: underline;
}}
.divider {{
height: 1px;
background: linear-gradient(to right, transparent, #e2e8f0, transparent);
margin: 30px 0;
}}
/* Responsive Design */
@media (max-width: 600px) {{
.email-wrapper {{
margin: 0 10px;
border-radius: 8px;
}}
.header, .content, .footer {{
padding: 20px;
}}
.logo {{
max-width: 150px;
}}
.header h1 {{
font-size: 24px;
}}
.subject-line {{
padding: 15px;
font-size: 16px;
}}
.cta-button {{
padding: 14px 28px;
font-size: 15px;
}}
}}
</style>
</head>
<body>
<div class='email-wrapper'>
<div class='header'>
<img src='https://i.ibb.co/F4DcSKm0/Logo-For-Email.png' alt='Nærværskonsulenterne Logo' class='logo' />
<h3>Nærværskonsulenterne</h1>
<p>Invitation til undersøgelse</p>
</div>
<div class='content'>
<div class='greeting'>Hej {userName}! 👋</div>
<div class='subject-line'>
{subject}
</div>
<p>Du inviteres til at deltage i en kort undersøgelse fra <strong>Nærværskonsulenterne</strong> vedrørende trivsel og samarbejde arbejdspladsen.</p>
<p>Din deltagelse er meget værdifuld for os, og vi sætter stor pris din tid og feedback. Undersøgelsen tager kun minutter at gennemføre.</p>
<div class='cta-section'>
<a href='{url}' class='cta-button'>Start spørgeskema</a>
</div>
<div class='expiry-notice'>
<span class='icon'></span>
<p>Vigtigt: Skemaet udløber den {expiryDate} kl. {expiryTime}</p>
</div>
<p>Hvis du har spørgsmål eller brug for hjælp, er du velkommen til at kontakte os.</p>
<p> forhånd tak for din deltagelse!</p>
</div>
<div class='divider'></div>
<div class='footer'>
<div class='company-info'>
<h3>Nærværskonsulenterne ApS</h3>
<p>Brødemosevej 24A<br/>
3300 Frederiksværk<br/>
Danmark</p>
<br/>
<p>📧 E-mail: <a href='mailto:kontakt@nvkn.dk'>kontakt@nvkn.dk</a></p>
</div>
</div>
</div>
</body>
</html>";
}

View file

@ -12,7 +12,7 @@
<div class="container mt-4"> <div class="container mt-4">
<div class="card justify-content-center"> <div class="card justify-content-center">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Update banner</h5> <h5 class="card-title">Update Footer</h5>
<div class="row "> <div class="row ">
<!-- 12 columns for textboxes --> <!-- 12 columns for textboxes -->

View file

@ -1,14 +1,10 @@
using Data; using Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Model; using Model;
using OpenAI_API; using OpenAI_API;
using Services.Implemnetation; using Services.Implemnetation;
using Services.Interaces; using Services.Interaces;
using System.Net;
using Web.AIConfiguration; using Web.AIConfiguration;
namespace Web.Extesions namespace Web.Extesions

View file

@ -8,6 +8,7 @@
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/Web.styles.css" asp-append-version="true" /> <link rel="stylesheet" href="~/Web.styles.css" asp-append-version="true" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css" /> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css" />
</head> </head>
<body> <body>

View file

@ -13,7 +13,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="EPPlus" Version="7.1.3" /> <PackageReference Include="EPPlus" Version="7.1.3" />
<PackageReference Include="itext7" Version="8.0.4" /> <PackageReference Include="itext7" Version="9.2.0" />
<PackageReference Include="iTextSharp" Version="5.5.13.3" /> <PackageReference Include="iTextSharp" Version="5.5.13.3" />
<PackageReference Include="MailJet.Api" Version="3.0.0" /> <PackageReference Include="MailJet.Api" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter" Version="8.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter" Version="8.0.2" />

View file

@ -17,7 +17,7 @@
// "SurveyVista": "data source=SQL1003.site4now.net; Initial Catalog=db_ab8a17_vistasurvey;User Id=db_ab8a17_vistasurvey_admin,Password=1!QaisYousuf;integrated security=True; TrustServerCertificate=True;" // "SurveyVista": "data source=SQL1003.site4now.net; Initial Catalog=db_ab8a17_vistasurvey;User Id=db_ab8a17_vistasurvey_admin,Password=1!QaisYousuf;integrated security=True; TrustServerCertificate=True;"
//}, //},
"Email": { "Email": {
"From": "mr.qais.yousuf@gmail.com", "From": "survey@asurvey.dk",
"ApplicationName": "Online Survey", "ApplicationName": "Online Survey",
"ConfirmEmailPath": "Subscription/Confirmation", "ConfirmEmailPath": "Subscription/Confirmation",
"unsubscribePath": "Subscription/UnsubscribeConfirmation", "unsubscribePath": "Subscription/UnsubscribeConfirmation",
@ -25,8 +25,8 @@
}, },
"MailJet": { "MailJet": {
"ApiKey": "f545eee3a4743464b9d25fb9c5ab3f6c", "ApiKey": "f06e28f892a81377545360662d8fe250",
"SecretKey": "8df3cf0337a090b1d6301f312ca51413" "SecretKey": "244883216ed68f83d2d4107bc53c6484"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -35,6 +35,182 @@
--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
} }
/*_______________________________________________________________________________start of the custom CSS_____________________________________________________________*/ /*_______________________________________________________________________________start of the custom CSS_____________________________________________________________*/
/* Additional Footer Improvements - Compact Version */
/* Footer section styling */
footer {
background: linear-gradient(135deg, #141c27 0%, #1a2332 100%);
position: relative;
}
footer::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, #33b3ae, transparent);
}
/* Compact container spacing */
.container-fluid.px-4 {
padding-left: 3rem !important;
padding-right: 3rem !important;
}
@media (max-width: 768px) {
.container-fluid.px-4 {
padding-left: 1.5rem !important;
padding-right: 1.5rem !important;
}
}
@media (max-width: 576px) {
.container-fluid.px-4 {
padding-left: 1rem !important;
padding-right: 1rem !important;
}
}
/* Compact footer headings */
footer h5 {
color: #33b3ae !important;
font-weight: 600;
font-size: 1.1rem;
position: relative;
padding-bottom: 5px;
}
footer h5::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 25px;
height: 2px;
background: #33b3ae;
}
footer h6 {
color: #ffffff !important;
font-weight: 500;
font-size: 0.95rem;
}
/* Compact social media links styling */
footer .d-flex a {
padding: 6px 10px;
border: 1px solid rgba(51, 179, 174, 0.3);
border-radius: 4px;
transition: all 0.3s ease;
background: rgba(51, 179, 174, 0.1);
font-size: 0.85rem;
}
footer .d-flex a:hover {
background: #33b3ae;
color: #ffffff !important;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(51, 179, 174, 0.3);
}
/* Compact navigation links styling */
footer .nav-item a {
transition: all 0.3s ease;
padding: 2px 0;
display: block;
font-size: 0.9rem;
}
footer .nav-item a:hover {
color: #33b3ae !important;
padding-left: 5px;
}
/* Compact contact links styling */
footer a[href^="mailto:"],
footer a[href^="tel:"] {
transition: all 0.3s ease;
font-size: 0.85rem;
}
footer a[href^="mailto:"]:hover,
footer a[href^="tel:"]:hover {
color: #33b3ae !important;
}
/* Compact bottom section styling */
footer hr {
margin: 1rem 0 0.5rem 0;
opacity: 0.3;
}
footer .row:last-child span {
font-size: 0.8rem;
display: flex;
align-items: center;
}
/* Compact text styling */
footer .small {
font-size: 0.85rem !important;
}
footer .text-muted.small {
font-size: 0.8rem !important;
}
/* Responsive improvements for compact design */
@media (max-width: 992px) {
#rowSection {
flex-direction: column;
}
#box {
width: 100% !important;
margin-bottom: 1.5rem;
}
footer .container-fluid .row {
text-align: center;
}
footer .d-flex {
justify-content: center !important;
}
}
/* Compact animation for footer elements */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
footer .col-lg-3,
footer .col-lg-2,
footer .col-lg-4 {
animation: fadeInUp 0.5s ease forwards;
}
footer .col-lg-3:nth-child(1) {
animation-delay: 0.1s;
}
footer .col-lg-2:nth-child(2) {
animation-delay: 0.15s;
}
footer .col-lg-4:nth-child(3) {
animation-delay: 0.2s;
}
#Background { #Background {
background-color: #141c27 !important; background-color: #141c27 !important;
} }
@ -420,6 +596,8 @@ body, html {
} }
/*_______________________________________________________________________________end of the custom CSS_____________________________________________________________*/ /*_______________________________________________________________________________end of the custom CSS_____________________________________________________________*/
*, *,
*::before, *::before,
*::after { *::after {