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> providerNames,
|
||||
string language,
|
||||
string? location,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var operatorCopy = _emailTemplates.GetOperatorCopy("email.search-results.subject", language);
|
||||
@@ -58,7 +59,7 @@ public sealed class CvSearchEmailSender
|
||||
|
||||
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,
|
||||
("count", results.Count.ToString()));
|
||||
|
||||
@@ -87,9 +88,9 @@ public sealed class CvSearchEmailSender
|
||||
/// Returns the empty-results template when no results are present.
|
||||
/// Prepends a scan summary block showing the keywords and providers used.
|
||||
/// </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)
|
||||
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.
|
||||
/// Keyword tags are built here because they are variable-count inline elements, not structural HTML.
|
||||
/// </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
|
||||
? string.Join(" ", keywords.Select(k =>
|
||||
@@ -132,9 +133,12 @@ public sealed class CvSearchEmailSender
|
||||
? string.Join(", ", providerNames)
|
||||
: "none";
|
||||
|
||||
var locationDisplay = string.IsNullOrWhiteSpace(location) ? "-" : location;
|
||||
|
||||
return _emailTemplates.Render("email.search-results.scan-summary", language,
|
||||
("keywordsHtml", keywordsHtml),
|
||||
("providers", providers));
|
||||
("providers", providers),
|
||||
("location", locationDisplay));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -111,6 +111,7 @@ public sealed class CvSearchJobTask : IJobTask
|
||||
cvKeywords,
|
||||
providers.Select(p => p.Name).ToList(),
|
||||
pending.Language,
|
||||
pending.Location,
|
||||
cancellationToken);
|
||||
|
||||
_logger.LogInformation("Session {SessionId} done. {Count} results sent.", pending.Id, results.Count);
|
||||
|
||||
Reference in New Issue
Block a user