Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
@inject AuthenticationStateProvider AuthStateProvider
@inject AuthService AuthService
@inject IConfiguration Configuration
@inject NavigationManager NavigationManager
@inject IJSRuntime JS

<div class="page">
<div class="sidebar">
Expand Down Expand Up @@ -61,6 +59,3 @@
<a href="." class="reload">Reload</a>
<span class="dismiss">X</span>
</div>

@code {
}
5 changes: 0 additions & 5 deletions Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,6 @@
<span class="bi bi-file-earmark-text-nav-menu" aria-hidden="true"></span> JSA Report
</NavLink>
</div>
<style>
.bi-file-earmark-text-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z'/%3E%3Cpath d='M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z'/%3E%3C/svg%3E");
}
</style>

@if (Configuration.GetValue<bool>("LocalMode"))
{
Expand Down
5 changes: 3 additions & 2 deletions Components/Layout/UpdateBanner.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
@inject UpdateCheckService UpdateCheckService
@inject NavigationManager NavigationManager
@inject IJSRuntime JS
@inject IHttpClientFactory HttpClientFactory

@if (UpdateCheckService.UpdateAvailable)
{
<div class="update-banner">
<i class="bi bi-arrow-up-circle-fill"></i>
A new version (<strong>@UpdateCheckService.LatestVersion</strong>) is available!
<a href="javascript:void(0)" @onclick="DownloadUpdate">Download &amp; restart</a>
<button type="button" class="btn btn-link p-0 align-baseline" @onclick="DownloadUpdate">Download &amp; restart</button>
</div>
}

Expand All @@ -27,7 +28,7 @@
await JS.InvokeVoidAsync("open", url, "_blank");
}

using var http = new HttpClient();
using var http = HttpClientFactory.CreateClient();
var baseUri = new Uri(NavigationManager.BaseUri);
await http.PostAsync(new Uri(baseUri, "/api/update-download"), null);
}
Expand Down
249 changes: 239 additions & 10 deletions Components/Pages/JsaReport.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject JsaReportService ReportService
@inject JobHistoryService HistoryService
@inject AppSettingsService SettingsService
@inject EmailService EmailService
@inject NavigationManager Navigation
@inject IJSRuntime JSRuntime
@rendermode InteractiveServer
Expand All @@ -27,10 +30,19 @@
<i class="bi bi-file-earmark-pdf"></i> PDF
</button>
</div>
@if (!string.IsNullOrEmpty(jsaSettings.WorkCoachEmail))
{
<button class="btn btn-outline-primary" @onclick="EmailReport" disabled="@isExporting" title="Email PDF report to @jsaSettings.WorkCoachName">
<i class="bi bi-envelope"></i> Email to @(string.IsNullOrEmpty(jsaSettings.WorkCoachName) ? "Work Coach" : jsaSettings.WorkCoachName)
</button>
}
}
<button class="btn btn-outline-secondary" @onclick="GenerateReport">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
<button class="btn btn-outline-primary" @onclick="() => showAddContactModal = true">
<i class="bi bi-person-plus"></i> Add Contact/Discussion
</button>
</div>
</div>

Expand Down Expand Up @@ -104,6 +116,10 @@
<button class="btn btn-sm btn-outline-primary" @onclick="() => SetDateRange(30)">Last 30 days</button>
<button class="btn btn-sm btn-outline-primary" @onclick="() => SetDateRange(90)">Last 3 months</button>
<button class="btn btn-sm btn-outline-secondary" @onclick="SetCurrentSigningPeriod">Current Signing Period</button>
@if (jsaSettings.SigningStartDate.HasValue && jsaSettings.PeriodLengthDays > 0)
{
<button class="btn btn-sm btn-outline-secondary" @onclick="SetPreviousSigningPeriod">Previous Signing Period</button>
}
</div>
</div>
</div>
Expand Down Expand Up @@ -249,7 +265,21 @@
@JsaReportService.GetActionTypeDisplay(entry.ActionType)
</span>
</td>
<td><small>@entry.Details</small></td>
<td>
<small>@entry.Details</small>
@if (!string.IsNullOrEmpty(entry.ContactName))
{
<br /><small class="text-muted"><strong>Contact:</strong> @entry.ContactName</small>
}
@if (!string.IsNullOrEmpty(entry.ContactReason))
{
<br /><small class="text-muted"><strong>Reason:</strong> @entry.ContactReason</small>
}
@if (!string.IsNullOrEmpty(entry.ContactResult))
{
<br /><small class="text-muted"><strong>Result:</strong> @entry.ContactResult</small>
}
</td>
<td>
@if (!string.IsNullOrEmpty(entry.OldValue) && !string.IsNullOrEmpty(entry.NewValue))
{
Expand All @@ -269,9 +299,16 @@

@if (!string.IsNullOrEmpty(exportError))
{
<div class="alert alert-danger alert-dismissible mt-3">
<div class="alert alert-danger alert-dismissible mt-3" role="alert">
<i class="bi bi-exclamation-triangle"></i> @exportError
<button type="button" class="btn-close" @onclick="() => exportError = null"></button>
<button type="button" class="btn-close" aria-label="Close" @onclick="() => exportError = null"></button>
</div>
}
@if (!string.IsNullOrEmpty(emailSuccessMessage))
{
<div class="alert alert-success alert-dismissible mt-3" role="alert">
<i class="bi bi-check-circle"></i> @emailSuccessMessage
<button type="button" class="btn-close" aria-label="Close" @onclick="() => emailSuccessMessage = null"></button>
</div>
}
</div>
Expand All @@ -286,12 +323,74 @@
</div>
}

@if (showAddContactModal)
{
<div class="modal fade show" style="display: block; background: rgba(0,0,0,0.5);" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-person-plus"></i> Add Contact/Discussion</h5>
<button type="button" class="btn-close" @onclick="CloseAddContactModal"></button>
Comment on lines +328 to +333
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modal close button is missing an accessible label. Add aria-label="Close" (and ideally ensure the modal container has appropriate dialog semantics like role="dialog"/aria-modal). This aligns with the accessibility improvements already made for the dismissible alerts on this page.

Suggested change
<div class="modal fade show" style="display: block; background: rgba(0,0,0,0.5);" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-person-plus"></i> Add Contact/Discussion</h5>
<button type="button" class="btn-close" @onclick="CloseAddContactModal"></button>
<div class="modal fade show" style="display: block; background: rgba(0,0,0,0.5);" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="addContactModalTitle">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addContactModalTitle"><i class="bi bi-person-plus"></i> Add Contact/Discussion</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="CloseAddContactModal"></button>

Copilot uses AI. Check for mistakes.
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Job Title or Role</label>
<input type="text" class="form-control" @bind="newContactJobTitle" placeholder="e.g., Senior Developer" />
</div>
<div class="mb-3">
<label class="form-label">Company/Recruiter</label>
<input type="text" class="form-control" @bind="newContactCompany" placeholder="e.g., Tech Corp Ltd" />
</div>
<div class="mb-3">
<label class="form-label">Contact Name</label>
<input type="text" class="form-control" @bind="newContactName" placeholder="e.g., John Smith" />
</div>
<div class="mb-3">
<label class="form-label">Reason for Contact</label>
<input type="text" class="form-control" @bind="newContactReason" placeholder="e.g., Phone screening, Follow-up email" />
</div>
<div class="mb-3">
<label class="form-label">Result/Outcome</label>
<textarea class="form-control" rows="3" @bind="newContactResult" placeholder="e.g., Arranged interview for next week"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Date/Time (optional)</label>
<input type="datetime-local" class="form-control" @bind="newContactTimestamp" />
<small class="form-text text-muted">Leave blank to use current date/time</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseAddContactModal">Cancel</button>
<button type="button" class="btn btn-primary" @onclick="SaveContactDiscussion" disabled="@(!IsContactFormValid)">
<i class="bi bi-save"></i> Save
</button>
</div>
</div>
</div>
</div>
}

@code {
private List<JsaReportGroup> reportGroups = new();
private JsaReportSummary? summary;
private bool isLoading = true;
private bool isExporting = false;
private string? exportError;
private string? emailSuccessMessage;

// Add Contact/Discussion modal
private bool showAddContactModal = false;
private string newContactJobTitle = "";
private string newContactCompany = "";
private string newContactName = "";
private string newContactReason = "";
private string newContactResult = "";
private DateTime? newContactTimestamp;

private bool IsContactFormValid =>
!string.IsNullOrWhiteSpace(newContactJobTitle) &&
!string.IsNullOrWhiteSpace(newContactCompany) &&
!string.IsNullOrWhiteSpace(newContactName);

// Filters
private DateTime? filterFromDate;
Expand All @@ -315,21 +414,45 @@
{ HistoryActionType.CompanyUpdated, "Company Updated" },
{ HistoryActionType.NotesUpdated, "Notes Updated" },
{ HistoryActionType.ContactAdded, "Contact Added" },
{ HistoryActionType.InteractionAdded, "Interaction Added" }
{ HistoryActionType.InteractionAdded, "Interaction Added" },
{ HistoryActionType.ContactDiscussion, "Contact/Discussion" }
};

private bool HasActiveFilters => filterFromDate.HasValue || filterToDate.HasValue ||
!string.IsNullOrEmpty(filterChangeSource) || !string.IsNullOrEmpty(filterSearchTerm) ||
!selectedActionTypes.SetEquals(JsaReportService.DefaultJsaActionTypes);

private JsaSettings jsaSettings = new();

protected override void OnInitialized()
{
// Default to last 14 days (typical signing period)
filterFromDate = DateTime.Today.AddDays(-14);
filterToDate = DateTime.Today;
jsaSettings = SettingsService.GetSettings().Jsa;
var period = GetCurrentSigningDates();
filterFromDate = period.from;
filterToDate = period.to;
GenerateReport();
}

private (DateTime from, DateTime to) GetCurrentSigningDates()
{
if (jsaSettings.SigningStartDate.HasValue && jsaSettings.PeriodLengthDays > 0)
{
var start = jsaSettings.SigningStartDate.Value.Date;
var periodDays = jsaSettings.PeriodLengthDays;
var daysSinceStart = (DateTime.Today - start).Days;
if (daysSinceStart >= 0)
{
var currentPeriodIndex = daysSinceStart / periodDays;
var periodStart = start.AddDays(currentPeriodIndex * periodDays);
var periodEnd = periodStart.AddDays(periodDays - 1);
// Return full period even if end date is in the future
return (periodStart, periodEnd);
}
}
// Fallback: last 14 days
return (DateTime.Today.AddDays(-14), DateTime.Today);
}

private void GenerateReport()
{
isLoading = true;
Expand Down Expand Up @@ -383,12 +506,38 @@

private void SetCurrentSigningPeriod()
{
// Typical JSA signing is fortnightly
filterFromDate = DateTime.Today.AddDays(-14);
filterToDate = DateTime.Today;
var period = GetCurrentSigningDates();
filterFromDate = period.from;
filterToDate = period.to;
GenerateReport();
}

private void SetPreviousSigningPeriod()
{
if (jsaSettings.SigningStartDate.HasValue && jsaSettings.PeriodLengthDays > 0)
{
var start = jsaSettings.SigningStartDate.Value.Date;
var periodDays = jsaSettings.PeriodLengthDays;
var daysSinceStart = (DateTime.Today - start).Days;
if (daysSinceStart >= 0)
{
var currentPeriodIndex = daysSinceStart / periodDays;
var previousPeriodIndex = currentPeriodIndex - 1;
if (previousPeriodIndex >= 0)
{
var periodStart = start.AddDays(previousPeriodIndex * periodDays);
var periodEnd = periodStart.AddDays(periodDays - 1);
filterFromDate = periodStart;
filterToDate = periodEnd;
GenerateReport();
return;
}
}
}
// Fallback if no previous period available
SetDateRange(14);
}

private void ClearFilters()
{
filterFromDate = null;
Expand Down Expand Up @@ -482,6 +631,85 @@
}
}

private async Task EmailReport()
{
isExporting = true;
exportError = null;
emailSuccessMessage = null;
StateHasChanged();
await Task.Delay(50);

try
{
var smtpSettings = SettingsService.GetSettingsDecrypted();
if (string.IsNullOrEmpty(smtpSettings.SmtpHost))
{
exportError = "SMTP is not configured. Please configure email settings first.";
return;
}

var pdfBytes = ReportService.ExportToPdf(reportGroups, summary!, GetAppBaseUrl());
var fileName = $"JSA-Report-{DateTime.Now:yyyy-MM-dd}.pdf";
var periodText = $"{summary!.DateFrom:dd/MM/yyyy} - {summary.DateTo:dd/MM/yyyy}";

var htmlBody = $@"<p>Please find attached my JSA Job Search Activity Report for the period <strong>{periodText}</strong>.</p>
<p>Summary: {summary.TotalJobs} jobs tracked, {summary.JobsAppliedTo} applied to, {summary.TotalActivities} total activities.</p>
<p>Kind regards</p>";

var sent = await EmailService.SendEmailWithAttachmentAsync(
jsaSettings.WorkCoachEmail,
$"JSA Job Search Report - {periodText}",
htmlBody,
smtpSettings.SmtpHost, smtpSettings.SmtpPort,
smtpSettings.SmtpUsername, smtpSettings.SmtpPassword,
smtpSettings.SmtpFromEmail, smtpSettings.SmtpFromName,
pdfBytes, fileName, "application/pdf");

if (sent)
emailSuccessMessage = $"Report emailed to {jsaSettings.WorkCoachEmail}";
else
exportError = "Failed to send email. Check SMTP settings.";
}
catch (Exception ex)
{
exportError = $"Email failed: {ex.Message}";
}
finally
{
isExporting = false;
StateHasChanged();
}
}

private void CloseAddContactModal()
{
showAddContactModal = false;
newContactJobTitle = "";
newContactCompany = "";
newContactName = "";
newContactReason = "";
newContactResult = "";
newContactTimestamp = null;
}

private void SaveContactDiscussion()
{
if (!IsContactFormValid)
return;

HistoryService.RecordStandaloneContactDiscussion(
newContactJobTitle,
newContactCompany,
newContactName,
newContactReason,
newContactResult,
newContactTimestamp
);

CloseAddContactModal();
GenerateReport(); // Refresh the report to show the new entry
}

private static string GetActionBadgeClass(HistoryActionType action)
{
return action switch
Expand All @@ -491,6 +719,7 @@
HistoryActionType.ApplicationStageChanged => "bg-info",
HistoryActionType.InterestChanged => "bg-warning text-dark",
HistoryActionType.SuitabilityChanged => "bg-secondary",
HistoryActionType.ContactDiscussion => "bg-dark",
_ => "bg-light text-dark"
};
}
Expand Down
Loading
Loading