Fix Outlook email layout and move all HTML/prompts out of code #38

Merged
claude merged 8 commits from main into staging 2026-06-01 17:30:18 +00:00
2 changed files with 112 additions and 23 deletions
Showing only changes of commit 978dd3a069 - Show all commits
+12 -7
View File
@@ -6,6 +6,7 @@ using EmailApi.Models.Requests;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Models.Requests; using Models.Requests;
using Models.Settings; using Models.Settings;
using System.Net;
namespace Api.Services; namespace Api.Services;
@@ -194,31 +195,35 @@ public sealed class EmailApiEmailSender : IEmailSender
/// <inheritdoc /> /// <inheritdoc />
public string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel, string language, string? jobSearchLink = null, int expiryDays = 7) public string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel, string language, string? jobSearchLink = null, int expiryDays = 7)
{ {
// Build HTML lists for strengths, gaps, and recommendations
var strengths = result.Strengths?.Count > 0 var strengths = result.Strengths?.Count > 0
? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8\">" + ? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8;margin:0\">" +
string.Join("", result.Strengths.Select(s => $"<li>{s}</li>")) + "</ul>" string.Join("", result.Strengths.Select(s => $"<li>{s}</li>")) + "</ul>"
: "<p style=\"color:#6c757d\">—</p>"; : "<p style=\"color:#6c757d;margin:0\">—</p>";
var gaps = result.Gaps?.Count > 0 var gaps = result.Gaps?.Count > 0
? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8\">" + ? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8;margin:0\">" +
string.Join("", result.Gaps.Select(g => $"<li>{g}</li>")) + "</ul>" string.Join("", result.Gaps.Select(g => $"<li>{g}</li>")) + "</ul>"
: "<p style=\"color:#6c757d\">—</p>"; : "<p style=\"color:#6c757d;margin:0\">—</p>";
var recommendations = result.Recommendations?.Count > 0 var recommendations = result.Recommendations?.Count > 0
? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8\">" + ? "<ul style=\"color:#495057;padding-left:20px;line-height:1.8;margin:0\">" +
string.Join("", result.Recommendations.Select(r => $"<li>{r}</li>")) + "</ul>" string.Join("", result.Recommendations.Select(r => $"<li>{r}</li>")) + "</ul>"
: "<p style=\"color:#6c757d\">—</p>"; : "<p style=\"color:#6c757d;margin:0\">—</p>";
// Render the HTML template with substituted values
// email.match.body is now stored as HTML in the database
var body = _emailTemplates.Render("email.match.body", language, var body = _emailTemplates.Render("email.match.body", language,
("cvDocumentId", cvDocumentId), ("cvDocumentId", cvDocumentId),
("jobLabel", jobLabel ?? "N/A"), ("jobLabel", jobLabel ?? "N/A"),
("jobUrl", result.JobUrl ?? "N/A"), ("jobUrl", result.JobUrl ?? "N/A"),
("score", result.Score.ToString()), ("score", result.Score.ToString()),
("summary", result.Summary ?? string.Empty), ("summary", WebUtility.HtmlEncode(result.Summary ?? string.Empty)),
("strengths", strengths), ("strengths", strengths),
("gaps", gaps), ("gaps", gaps),
("recommendations", recommendations)); ("recommendations", recommendations));
// Append the job search footer if link is provided
if (!string.IsNullOrWhiteSpace(jobSearchLink)) if (!string.IsNullOrWhiteSpace(jobSearchLink))
{ {
body += _emailTemplates.Render("email.match.job-search-footer", language, body += _emailTemplates.Render("email.match.job-search-footer", language,
@@ -44,33 +44,117 @@ namespace Email.Data.Migrations
Row("email.match.subject", "en", "MyAi.ro CV Match: {{score}}% - {{jobLabel}}", "Subject for the CV match result email"); Row("email.match.subject", "en", "MyAi.ro CV Match: {{score}}% - {{jobLabel}}", "Subject for the CV match result email");
Row("email.match.subject", "ro", "MyAi.ro Potrivire CV: {{score}}% - {{jobLabel}}", "Subiect email rezultat potrivire CV"); Row("email.match.subject", "ro", "MyAi.ro Potrivire CV: {{score}}% - {{jobLabel}}", "Subiect email rezultat potrivire CV");
// Match result email — body // Match result email — body (HTML formatted)
Row("email.match.body", "en", Row("email.match.body", "en",
"CV Matcher result\n\nCV Document ID: {{cvDocumentId}}\nJob: {{jobLabel}}\nJob URL: {{jobUrl}}\nScore: {{score}}%\n\nSummary:\n{{summary}}\n\nStrengths:\n{{strengths}}\n\nGaps:\n{{gaps}}\n\nRecommendations:\n{{recommendations}}", @"<h2 style=""color: #2c5282; margin-bottom: 20px; border-bottom: 3px solid #2c5282; padding-bottom: 10px;"">CV Match Report</h2>
"Body for the CV match result email"); <table style=""width: 100%; border-collapse: collapse; margin-bottom: 30px; border: 1px solid #ddd;"">
<tr style=""background-color: #2c5282; color: white;"">
<td style=""padding: 12px 15px; font-weight: bold; width: 35%;"">CV ID</td>
<td style=""padding: 12px 15px;"">{{cvDocumentId}}</td>
</tr>
<tr style=""background-color: #f8f9fa;"">
<td style=""padding: 12px 15px; font-weight: bold;"">Job</td>
<td style=""padding: 12px 15px;"">{{jobLabel}}</td>
</tr>
<tr>
<td style=""padding: 12px 15px; font-weight: bold;"">URL</td>
<td style=""padding: 12px 15px;""><a href=""{{jobUrl}}"" style=""color: #2c5282; text-decoration: none;"">{{jobUrl}}</a></td>
</tr>
<tr style=""background-color: #f8f9fa;"">
<td style=""padding: 12px 15px; font-weight: bold;"">Score</td>
<td style=""padding: 12px 15px; color: #27ae60; font-weight: bold; font-size: 18px;"">{{score}}%</td>
</tr>
</table>
<h3 style=""color: #2c5282; margin-top: 25px; margin-bottom: 10px; border-bottom: 2px solid #2c5282; padding-bottom: 5px;"">Summary</h3>
<p style=""line-height: 1.6; color: #333;"">{{summary}}</p>
<h3 style=""color: #2c5282; margin-top: 25px; margin-bottom: 10px; border-bottom: 2px solid #2c5282; padding-bottom: 5px;"">Strengths</h3>
<div style=""line-height: 1.8; color: #333;"">{{strengths}}</div>
<h3 style=""color: #2c5282; margin-top: 25px; margin-bottom: 10px; border-bottom: 2px solid #2c5282; padding-bottom: 5px;"">Gaps</h3>
<div style=""line-height: 1.8; color: #333;"">{{gaps}}</div>
<h3 style=""color: #2c5282; margin-top: 25px; margin-bottom: 10px; border-bottom: 2px solid #2c5282; padding-bottom: 5px;"">Recommendations</h3>
<div style=""line-height: 1.8; color: #333;"">{{recommendations}}</div>",
"Body for the CV match result email (HTML formatted)");
Row("email.match.body", "ro", Row("email.match.body", "ro",
"Rezultat potrivire CV\n\nID document CV: {{cvDocumentId}}\nJob: {{jobLabel}}\nURL job: {{jobUrl}}\nScor: {{score}}%\n\nRezumat:\n{{summary}}\n\nPuncte forte:\n{{strengths}}\n\nLipsuri:\n{{gaps}}\n\nRecomandări:\n{{recommendations}}", @"<h2 style=""color: #2c5282; margin-bottom: 20px; border-bottom: 3px solid #2c5282; padding-bottom: 10px;"">Report Potrivire CV</h2>
"Corpul emailului pentru rezultatul potrivirii CV"); <table style=""width: 100%; border-collapse: collapse; margin-bottom: 30px; border: 1px solid #ddd;"">
<tr style=""background-color: #2c5282; color: white;"">
<td style=""padding: 12px 15px; font-weight: bold; width: 35%;"">ID Document CV</td>
<td style=""padding: 12px 15px;"">{{cvDocumentId}}</td>
</tr>
<tr style=""background-color: #f8f9fa;"">
<td style=""padding: 12px 15px; font-weight: bold;"">Job</td>
<td style=""padding: 12px 15px;"">{{jobLabel}}</td>
</tr>
<tr>
<td style=""padding: 12px 15px; font-weight: bold;"">URL</td>
<td style=""padding: 12px 15px;""><a href=""{{jobUrl}}"" style=""color: #2c5282; text-decoration: none;"">{{jobUrl}}</a></td>
</tr>
<tr style=""background-color: #f8f9fa;"">
<td style=""padding: 12px 15px; font-weight: bold;"">Scor</td>
<td style=""padding: 12px 15px; color: #27ae60; font-weight: bold; font-size: 18px;"">{{score}}%</td>
</tr>
</table>
<h3 style=""color: #2c5282; margin-top: 25px; margin-bottom: 10px; border-bottom: 2px solid #2c5282; padding-bottom: 5px;"">Rezumat</h3>
<p style=""line-height: 1.6; color: #333;"">{{summary}}</p>
<h3 style=""color: #2c5282; margin-top: 25px; margin-bottom: 10px; border-bottom: 2px solid #2c5282; padding-bottom: 5px;"">Puncte Forte</h3>
<div style=""line-height: 1.8; color: #333;"">{{strengths}}</div>
<h3 style=""color: #2c5282; margin-top: 25px; margin-bottom: 10px; border-bottom: 2px solid #2c5282; padding-bottom: 5px;"">Lipsuri</h3>
<div style=""line-height: 1.8; color: #333;"">{{gaps}}</div>
<h3 style=""color: #2c5282; margin-top: 25px; margin-bottom: 10px; border-bottom: 2px solid #2c5282; padding-bottom: 5px;"">Recomandări</h3>
<div style=""line-height: 1.8; color: #333;"">{{recommendations}}</div>",
"Corpul emailului pentru rezultatul potrivirii CV (format HTML)");
// Match result email — job search CTA footer // Match result email — job search CTA footer (HTML formatted)
Row("email.match.job-search-footer", "en", Row("email.match.job-search-footer", "en",
"\n\n---\nWant to find more jobs matching your CV?\nClick: {{jobSearchLink}}\n(link valid for {{expiryDays}} days)", @"<hr style=""border: none; border-top: 1px solid #ddd; margin: 30px 0;"">
"Job search CTA appended to match result email"); <p style=""margin-top: 20px; font-size: 14px; color: #333;"">Want to find more jobs matching your CV?</p>
<p style=""margin-bottom: 20px;""><a href=""{{jobSearchLink}}"" style=""background-color: #2c5282; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; display: inline-block; font-weight: bold;"">Search Jobs</a></p>
<p style=""font-size: 12px; color: #666;"">(link valid for {{expiryDays}} days)</p>",
"Job search CTA appended to match result email (HTML formatted)");
Row("email.match.job-search-footer", "ro", Row("email.match.job-search-footer", "ro",
"\n\n---\nVrei sa gasesti mai multe joburi potrivite CV-ului tau?\nClick: {{jobSearchLink}}\n(link valabil {{expiryDays}} zile)", @"<hr style=""border: none; border-top: 1px solid #ddd; margin: 30px 0;"">
"CTA cautare joburi adaugat la emailul de potrivire CV"); <p style=""margin-top: 20px; font-size: 14px; color: #333;"">Vrei să găsești mai multe joburi potrivite CV-ului tău?</p>
<p style=""margin-bottom: 20px;""><a href=""{{jobSearchLink}}"" style=""background-color: #2c5282; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; display: inline-block; font-weight: bold;"">Caută Joburi</a></p>
<p style=""font-size: 12px; color: #666;"">(link valabil {{expiryDays}} zile)</p>",
"CTA cautare joburi adaugat la emailul de potrivire CV (format HTML)");
// Job search results email — subject // Job search results email — subject
Row("email.search-results.subject", "en", "MyAi.ro: {{count}} jobs matching your CV", "Subject for job search results email"); Row("email.search-results.subject", "en", "MyAi.ro: {{count}} jobs matching your CV", "Subject for job search results email");
Row("email.search-results.subject", "ro", "MyAi.ro: {{count}} joburi potrivite CV-ului tau", "Subiect email rezultate cautare joburi"); Row("email.search-results.subject", "ro", "MyAi.ro: {{count}} joburi potrivite CV-ului tau", "Subiect email rezultate cautare joburi");
// Job search results email — body preamble (items appended in code) // Job search results email — body preamble (items appended in code) - HTML formatted
Row("email.search-results.body", "en", "MyAi.ro found {{count}} jobs matching your CV:\n\n{{items}}", "Body preamble for job search results email"); Row("email.search-results.body", "en",
Row("email.search-results.body", "ro", "MyAi.ro a gasit {{count}} joburi potrivite CV-ului tau:\n\n{{items}}", "Corpul emailului de rezultate cautare joburi"); @"<h2 style=""color: #2c5282; margin-bottom: 20px; border-bottom: 3px solid #2c5282; padding-bottom: 10px;"">Job Search Results</h2>
<p style=""margin-bottom: 20px; color: #333;"">MyAi.ro found <strong>{{count}}</strong> jobs matching your CV:</p>
<div style=""margin-top: 20px; color: #333;"">
{{items}}
</div>",
"Body preamble for job search results email (HTML formatted)");
Row("email.search-results.body", "ro",
@"<h2 style=""color: #2c5282; margin-bottom: 20px; border-bottom: 3px solid #2c5282; padding-bottom: 10px;"">Rezultate Căutare Joburi</h2>
<p style=""margin-bottom: 20px; color: #333;"">MyAi.ro a găsit <strong>{{count}}</strong> joburi potrivite CV-ului tău:</p>
<div style=""margin-top: 20px; color: #333;"">
{{items}}
</div>",
"Corpul emailului de rezultate cautare joburi (format HTML)");
// Job search results email — no results found // Job search results email — no results found - HTML formatted
Row("email.search-results.empty", "en", "MyAi.ro found no jobs matching your CV. Try again later or update your CV.", "No results message for job search results email"); Row("email.search-results.empty", "en",
Row("email.search-results.empty", "ro", "MyAi.ro nu a gasit joburi care sa corespunda CV-ului tau. Incercati mai tarziu sau ajustati CV-ul.", "Mesaj fara rezultate pentru emailul de cautare joburi"); @"<div style=""background-color: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 15px; margin: 20px 0;"">
<p style=""margin: 0; color: #856404;"">
<strong>No jobs found</strong><br>
MyAi.ro found no jobs matching your CV at this moment. Please try again later or update your CV to improve your match results.
</p>
</div>",
"No results message for job search results email (HTML formatted)");
Row("email.search-results.empty", "ro",
@"<div style=""background-color: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 15px; margin: 20px 0;"">
<p style=""margin: 0; color: #856404;"">
<strong>Niciun job găsit</strong><br>
MyAi.ro nu a găsit joburi potrivite CV-ului tău în acest moment. Te rugăm să încerci din nou mai târziu sau să-ți actualizezi CV-ul pentru a obține rezultate mai bune.
</p>
</div>",
"Mesaj fara rezultate pentru emailul de cautare joburi (format HTML)");
// HTML job-search start page messages // HTML job-search start page messages
Row("html.job-search.started.title", "en", "Job search started", "Title for job search started page"); Row("html.job-search.started.title", "en", "Job search started", "Title for job search started page");