Add searched location to job search results email
Build and Push Docker Images Staging / build (push) Successful in 14m42s
Build and Push Docker Images Staging / build (push) Successful in 14m42s
Show the candidate's location in the scan summary block of the results email
alongside keywords and providers, for both en and ro templates.
- CvSearchEmailSender.SendResultsAsync accepts location and passes it to BuildScanSummary
- BuildScanSummary passes {{location}} to the template (falls back to '-' when absent)
- CvSearchJobTask passes session.Location to SendResultsAsync
- New migration AddLocationToScanSummaryTemplate updates both language variants of
email.search-results.scan-summary to include a 'Location / Locație căutată' row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+69
@@ -0,0 +1,69 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Email.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Email.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(EmailDbContext))]
|
||||||
|
[Migration("20260608125339_AddLocationToScanSummaryTemplate")]
|
||||||
|
partial class AddLocationToScanSummaryTemplate
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("email")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Email.Data.Entities.EmailTemplateEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("nvarchar(8)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)")
|
||||||
|
.HasDefaultValue("");
|
||||||
|
|
||||||
|
b.Property<string>("OperatorCopy")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)")
|
||||||
|
.HasDefaultValue("");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Key", "Language");
|
||||||
|
|
||||||
|
b.ToTable("Templates", "email");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Email.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddLocationToScanSummaryTemplate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.search-results.scan-summary", "en"],
|
||||||
|
columns: ["Value"],
|
||||||
|
values: [@"<table width=""100%"" cellpadding=""0"" cellspacing=""0"" border=""0"" style=""width: 100%; margin-bottom: 18px; border: 1px solid #dee2e6; border-collapse: collapse;"">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor=""#f8f9fa"" style=""background-color: #f8f9fa; padding: 14px 16px; font-size: 13px; color: #495057;"">
|
||||||
|
<div style=""margin-bottom: 8px;""><strong>Keywords used:</strong> {{keywordsHtml}}</div>
|
||||||
|
<div style=""margin-bottom: 8px;""><strong>Location:</strong> {{location}}</div>
|
||||||
|
<div><strong>Providers scanned:</strong> {{providers}}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>"]);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.search-results.scan-summary", "ro"],
|
||||||
|
columns: ["Value"],
|
||||||
|
values: [@"<table width=""100%"" cellpadding=""0"" cellspacing=""0"" border=""0"" style=""width: 100%; margin-bottom: 18px; border: 1px solid #dee2e6; border-collapse: collapse;"">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor=""#f8f9fa"" style=""background-color: #f8f9fa; padding: 14px 16px; font-size: 13px; color: #495057;"">
|
||||||
|
<div style=""margin-bottom: 8px;""><strong>Cuvinte cheie folosite:</strong> {{keywordsHtml}}</div>
|
||||||
|
<div style=""margin-bottom: 8px;""><strong>Locație căutată:</strong> {{location}}</div>
|
||||||
|
<div><strong>Furnizori scanați:</strong> {{providers}}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.search-results.scan-summary", "en"],
|
||||||
|
columns: ["Value"],
|
||||||
|
values: [@"<table width=""100%"" cellpadding=""0"" cellspacing=""0"" border=""0"" style=""width: 100%; margin-bottom: 18px; border: 1px solid #dee2e6; border-collapse: collapse;"">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor=""#f8f9fa"" style=""background-color: #f8f9fa; padding: 14px 16px; font-size: 13px; color: #495057;"">
|
||||||
|
<div style=""margin-bottom: 8px;""><strong>Keywords used:</strong> {{keywordsHtml}}</div>
|
||||||
|
<div><strong>Providers scanned:</strong> {{providers}}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>"]);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
schema: MigrationConstants.SchemaName,
|
||||||
|
table: "Templates",
|
||||||
|
keyColumns: ["Key", "Language"],
|
||||||
|
keyValues: ["email.search-results.scan-summary", "ro"],
|
||||||
|
columns: ["Value"],
|
||||||
|
values: [@"<table width=""100%"" cellpadding=""0"" cellspacing=""0"" border=""0"" style=""width: 100%; margin-bottom: 18px; border: 1px solid #dee2e6; border-collapse: collapse;"">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor=""#f8f9fa"" style=""background-color: #f8f9fa; padding: 14px 16px; font-size: 13px; color: #495057;"">
|
||||||
|
<div style=""margin-bottom: 8px;""><strong>Cuvinte cheie folosite:</strong> {{keywordsHtml}}</div>
|
||||||
|
<div><strong>Furnizori scanați:</strong> {{providers}}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ public sealed class CvSearchEmailSender
|
|||||||
IReadOnlyList<string> keywords,
|
IReadOnlyList<string> keywords,
|
||||||
IReadOnlyList<string> providerNames,
|
IReadOnlyList<string> providerNames,
|
||||||
string language,
|
string language,
|
||||||
|
string? location,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var operatorCopy = _emailTemplates.GetOperatorCopy("email.search-results.subject", language);
|
var operatorCopy = _emailTemplates.GetOperatorCopy("email.search-results.subject", language);
|
||||||
@@ -58,7 +59,7 @@ public sealed class CvSearchEmailSender
|
|||||||
|
|
||||||
if (recipients.Count == 0) return;
|
if (recipients.Count == 0) return;
|
||||||
|
|
||||||
var htmlBody = BuildBody(results, keywords, providerNames, language);
|
var htmlBody = BuildBody(results, keywords, providerNames, language, location);
|
||||||
var subject = _emailTemplates.Render("email.search-results.subject", language,
|
var subject = _emailTemplates.Render("email.search-results.subject", language,
|
||||||
("count", results.Count.ToString()));
|
("count", results.Count.ToString()));
|
||||||
|
|
||||||
@@ -87,9 +88,9 @@ public sealed class CvSearchEmailSender
|
|||||||
/// Returns the empty-results template when no results are present.
|
/// Returns the empty-results template when no results are present.
|
||||||
/// Prepends a scan summary block showing the keywords and providers used.
|
/// Prepends a scan summary block showing the keywords and providers used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string BuildBody(IReadOnlyList<JobSearchResultEntity> results, IReadOnlyList<string> keywords, IReadOnlyList<string> providerNames, string language)
|
private string BuildBody(IReadOnlyList<JobSearchResultEntity> results, IReadOnlyList<string> keywords, IReadOnlyList<string> providerNames, string language, string? location)
|
||||||
{
|
{
|
||||||
var scanSummary = BuildScanSummary(keywords, providerNames, language);
|
var scanSummary = BuildScanSummary(keywords, providerNames, language, location);
|
||||||
|
|
||||||
if (results.Count == 0)
|
if (results.Count == 0)
|
||||||
return scanSummary + _emailTemplates.Get("email.search-results.empty", language);
|
return scanSummary + _emailTemplates.Get("email.search-results.empty", language);
|
||||||
@@ -121,7 +122,7 @@ public sealed class CvSearchEmailSender
|
|||||||
/// Renders the scan summary block via template, passing keyword tags and provider list as data.
|
/// Renders the scan summary block via template, passing keyword tags and provider list as data.
|
||||||
/// Keyword tags are built here because they are variable-count inline elements, not structural HTML.
|
/// Keyword tags are built here because they are variable-count inline elements, not structural HTML.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string BuildScanSummary(IReadOnlyList<string> keywords, IReadOnlyList<string> providerNames, string language)
|
private string BuildScanSummary(IReadOnlyList<string> keywords, IReadOnlyList<string> providerNames, string language, string? location)
|
||||||
{
|
{
|
||||||
var keywordsHtml = keywords.Count > 0
|
var keywordsHtml = keywords.Count > 0
|
||||||
? string.Join(" ", keywords.Select(k =>
|
? string.Join(" ", keywords.Select(k =>
|
||||||
@@ -132,9 +133,12 @@ public sealed class CvSearchEmailSender
|
|||||||
? string.Join(", ", providerNames)
|
? string.Join(", ", providerNames)
|
||||||
: "none";
|
: "none";
|
||||||
|
|
||||||
|
var locationDisplay = string.IsNullOrWhiteSpace(location) ? "-" : location;
|
||||||
|
|
||||||
return _emailTemplates.Render("email.search-results.scan-summary", language,
|
return _emailTemplates.Render("email.search-results.scan-summary", language,
|
||||||
("keywordsHtml", keywordsHtml),
|
("keywordsHtml", keywordsHtml),
|
||||||
("providers", providers));
|
("providers", providers),
|
||||||
|
("location", locationDisplay));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ public sealed class CvSearchJobTask : IJobTask
|
|||||||
cvKeywords,
|
cvKeywords,
|
||||||
providers.Select(p => p.Name).ToList(),
|
providers.Select(p => p.Name).ToList(),
|
||||||
pending.Language,
|
pending.Language,
|
||||||
|
pending.Location,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
_logger.LogInformation("Session {SessionId} done. {Count} results sent.", pending.Id, results.Count);
|
_logger.LogInformation("Session {SessionId} done. {Count} results sent.", pending.Id, results.Count);
|
||||||
|
|||||||
Reference in New Issue
Block a user