diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index afe5b474b..a6e38d3fd 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -1275,7 +1275,21 @@ public class Controller : ControllerBase public ActionResult StopAlarmReviewCampaign() { AlarmReviewService.StopCampaign(); - return Ok(new { message = "Campaign stopped and progress file deleted. Safe to redeploy." }); + return Ok(new { message = "Campaign paused — progress preserved. Use ResumeAlarmReviewCampaign to restart timers." }); + } + + [HttpPost(nameof(ResumeAlarmReviewCampaign))] + public ActionResult ResumeAlarmReviewCampaign() + { + AlarmReviewService.ResumeCampaign(); + return Ok(new { message = "Campaign resumed — timers restarted from existing progress." }); + } + + [HttpPost(nameof(ResetAlarmReviewCampaign))] + public ActionResult ResetAlarmReviewCampaign() + { + AlarmReviewService.ResetCampaign(); + return Ok(new { message = "Campaign fully reset — all progress deleted. Use StartAlarmReviewCampaign to begin again." }); } [HttpGet(nameof(CorrectAlarm))] diff --git a/csharp/App/Backend/Services/AlarmReviewService.cs b/csharp/App/Backend/Services/AlarmReviewService.cs index f66fdcd96..ff278c407 100644 --- a/csharp/App/Backend/Services/AlarmReviewService.cs +++ b/csharp/App/Backend/Services/AlarmReviewService.cs @@ -206,8 +206,33 @@ public static class AlarmReviewService Console.WriteLine("[AlarmReviewService] Daily scheduler started (8AM + 2PM jobs)."); } - /// Stops the scheduler and deletes the progress file. Safe to call at any time. + /// Pauses the scheduler (timers stopped) but keeps all progress. Resume with ResumeCampaign(). public static void StopCampaign() + { + _morningTimer?.Dispose(); + _afternoonTimer?.Dispose(); + _morningTimer = null; + _afternoonTimer = null; + _testBatch = null; + Console.WriteLine("[AlarmReviewService] Campaign paused — progress preserved. Call ResumeCampaign() to restart timers."); + } + + /// Resumes timers without touching progress. Use after StopCampaign() or server restart. + public static void ResumeCampaign() + { + if (!File.Exists(ProgressFile)) + { + Console.WriteLine("[AlarmReviewService] No progress file found — use StartCampaign() to begin."); + return; + } + LoadGermanNames(); + ScheduleTimer(ref _morningTimer, 8, 0, () => RunMorningJobAsync() .GetAwaiter().GetResult()); + ScheduleTimer(ref _afternoonTimer, 14, 0, () => RunAfternoonJobAsync().GetAwaiter().GetResult()); + Console.WriteLine("[AlarmReviewService] Campaign resumed — timers restarted."); + } + + /// Deletes all progress and stops timers. Only use to start over from scratch. + public static void ResetCampaign() { _morningTimer?.Dispose(); _afternoonTimer?.Dispose(); @@ -217,8 +242,10 @@ public static class AlarmReviewService if (File.Exists(ProgressFile)) File.Delete(ProgressFile); + if (File.Exists(CheckedFilePath)) + File.Delete(CheckedFilePath); - Console.WriteLine("[AlarmReviewService] Campaign stopped and progress file deleted."); + Console.WriteLine("[AlarmReviewService] Campaign fully reset — all progress deleted."); } private static void ScheduleTimer(ref Timer? timer, int hour, int minute, Action action) @@ -265,6 +292,15 @@ public static class AlarmReviewService { var submissionCount = current.Submissions.Values.Count(s => s != null); + // If the batch was sent today, don't treat 0 submissions as a stall — + // reviewers haven't had a full day yet. Skip and wait for tomorrow's job. + var sentToday = current.SentDate == DateTime.Now.ToString("yyyy-MM-dd"); + if (submissionCount == 0 && sentToday) + { + Console.WriteLine($"[AlarmReviewService] Batch {current.BatchNumber}: sent today, 0 submissions so far — waiting for tomorrow."); + return; + } + if (submissionCount == 0) { const int MaxResends = 3; @@ -1457,6 +1493,46 @@ render(); var html = BuildReviewerEmailHtml(name, reviewUrl, batch.BatchNumber, batch.AlarmKeys.Count, quote, isResend); await SendEmailAsync(email, subject, html); } + + // Notify admin with a preview of what reviewers received + await SendAdminBatchDispatchNoticeAsync(batch, isResend); + } + + private static async Task SendAdminBatchDispatchNoticeAsync(BatchRecord batch, bool isResend) + { + var totalBatches = (int)Math.Ceiling((double)AllAlarmKeys.Length / BatchSize); + var action = isResend ? "erneut gesendet" : "gesendet"; + var subject = $"[Admin] Stapel {batch.BatchNumber}/{totalBatches} {action} an {Reviewers.Length} Prüfer"; + + var previewUrl = $"{BaseUrl}/ReviewAlarms?batch={batch.BatchNumber}&reviewer={Uri.EscapeDataString(Reviewers[0].Name)}"; + var reviewerList = string.Join(", ", Reviewers.Select(r => r.Name)); + + var alarmRows = string.Join("\n", batch.AlarmKeys.Select((key, i) => + $"""{i + 1}. {SplitCamelCase(key)} ({(SinexcelKeySet.Contains(key) ? "Sinexcel" : "Growatt")})""")); + + var html = $""" + + +

Stapel {batch.BatchNumber} {action}

+

{DateTime.Now:yyyy-MM-dd HH:mm} · {batch.AlarmKeys.Count} Alarme · Deadline: 8:00 Uhr morgen früh

+ +

Gesendet an: {reviewerList}

+ +

+ Formular ansehen → + (alle Prüfer sehen denselben Inhalt) +

+ +

Alarme in diesem Stapel

+ + {alarmRows} +
+ +

Diese E-Mail dient nur zur Information — keine Aktion erforderlich.

+ + """; + + await SendEmailAsync(AdminEmail, subject, html); } private static async Task SendReminderEmailAsync(BatchRecord batch, string name, string email)