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
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,15 @@
<data name="AutoMessageSeverityHelp" xml:space="preserve">
<value>التنبيهات بهذه الخطورة أو أعلى سترسل تلقائياً رسالة إلى جميع أعضاء القسم. قيمة أقل = خطورة أعلى (شديدة للغاية=0، شديدة=1، معتدلة=2، طفيفة=3).</value>
</data>
<data name="ExcludedEventsLabel" xml:space="preserve">
<value>أحداث التنبيه المستبعدة</value>
</data>
<data name="ExcludedEventsPlaceholder" xml:space="preserve">
<value>مثال: تحذير رياح البحيرة، تحذير الرياح</value>
</data>
<data name="ExcludedEventsHelp" xml:space="preserve">
<value>قائمة مفصولة بفواصل لعناوين أحداث التنبيه التي لا ينبغي أن تولد رسائل تلقائية. ستظل التنبيهات تظهر في لوحة المعلومات ولكن لن يتم إنشاء رسالة Resgrid لهذه الأحداث.</value>
</data>
<data name="EnableCallIntegration" xml:space="preserve">
<value>إرفاق سياق الطقس بالمكالمات</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,15 @@
<data name="AutoMessageSeverityHelp" xml:space="preserve">
<value>Warnungen mit diesem oder höherem Schweregrad senden automatisch eine Nachricht an alle Abteilungsmitglieder. Niedrigerer Wert = höherer Schweregrad (Extrem=0, Schwer=1, Mäßig=2, Gering=3).</value>
</data>
<data name="ExcludedEventsLabel" xml:space="preserve">
<value>Ausgeschlossene Alarmereignisse</value>
</data>
<data name="ExcludedEventsPlaceholder" xml:space="preserve">
<value>z.B. Windwarnung am See, Windwarnung</value>
</data>
<data name="ExcludedEventsHelp" xml:space="preserve">
<value>Kommagetrennte Liste von Alarm-Ereignistiteln, für die keine automatischen Nachrichten erstellt werden sollen. Die Alarme werden weiterhin im Dashboard angezeigt, aber es wird keine Resgrid-Nachricht für diese Ereignisse erstellt.</value>
</data>
<data name="EnableCallIntegration" xml:space="preserve">
<value>Wetterkontext an Einsätze anhängen</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,15 @@
<data name="AutoMessageSeverityHelp" xml:space="preserve">
<value>Alerts at or above this severity will automatically send a message to all department members. Lower value = higher severity (Extreme=0, Severe=1, Moderate=2, Minor=3).</value>
</data>
<data name="ExcludedEventsLabel" xml:space="preserve">
<value>Excluded Alert Events</value>
</data>
<data name="ExcludedEventsPlaceholder" xml:space="preserve">
<value>e.g. Lake Wind Advisory, Wind Advisory</value>
</data>
<data name="ExcludedEventsHelp" xml:space="preserve">
<value>Comma-separated list of alert event titles that should not generate auto-messages. Alerts will still appear in the dashboard but no Resgrid message will be created for these events.</value>
</data>
<data name="EnableCallIntegration" xml:space="preserve">
<value>Attach Weather Context to Calls</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,15 @@
<data name="AutoMessageSeverityHelp" xml:space="preserve">
<value>Las alertas con esta severidad o superior enviarán automáticamente un mensaje a todos los miembros del departamento. Valor más bajo = mayor severidad (Extrema=0, Severa=1, Moderada=2, Menor=3).</value>
</data>
<data name="ExcludedEventsLabel" xml:space="preserve">
<value>Eventos de Alerta Excluidos</value>
</data>
<data name="ExcludedEventsPlaceholder" xml:space="preserve">
<value>ej. Aviso de Viento en Lago, Aviso de Viento</value>
</data>
<data name="ExcludedEventsHelp" xml:space="preserve">
<value>Lista de títulos de eventos de alerta separados por comas que no deben generar mensajes automáticos. Las alertas seguirán apareciendo en el panel pero no se creará ningún mensaje de Resgrid para estos eventos.</value>
</data>
<data name="EnableCallIntegration" xml:space="preserve">
<value>Adjuntar Contexto Meteorológico a Llamadas</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,15 @@
<data name="AutoMessageSeverityHelp" xml:space="preserve">
<value>Les alertes de cette sévérité ou supérieure enverront automatiquement un message à tous les membres du département. Valeur plus basse = sévérité plus élevée (Extrême=0, Sévère=1, Modérée=2, Mineure=3).</value>
</data>
<data name="ExcludedEventsLabel" xml:space="preserve">
<value>Événements d'alerte exclus</value>
</data>
<data name="ExcludedEventsPlaceholder" xml:space="preserve">
<value>ex. Avis de vent lacustre, Avis de vent</value>
</data>
<data name="ExcludedEventsHelp" xml:space="preserve">
<value>Liste de titres d'événements d'alerte séparés par des virgules qui ne doivent pas générer de messages automatiques. Les alertes apparaîtront toujours dans le tableau de bord mais aucun message Resgrid ne sera créé pour ces événements.</value>
</data>
<data name="EnableCallIntegration" xml:space="preserve">
<value>Joindre le Contexte Météo aux Appels</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,15 @@
<data name="AutoMessageSeverityHelp" xml:space="preserve">
<value>Le allerte con questa gravità o superiore invieranno automaticamente un messaggio a tutti i membri del dipartimento. Valore più basso = gravità più alta (Estrema=0, Grave=1, Moderata=2, Lieve=3).</value>
</data>
<data name="ExcludedEventsLabel" xml:space="preserve">
<value>Eventi di allerta esclusi</value>
</data>
<data name="ExcludedEventsPlaceholder" xml:space="preserve">
<value>es. Avviso vento sul lago, Avviso di vento</value>
</data>
<data name="ExcludedEventsHelp" xml:space="preserve">
<value>Elenco separato da virgole di titoli di eventi di allerta che non devono generare messaggi automatici. Gli avvisi continueranno ad apparire nel pannello ma non verrà creato alcun messaggio Resgrid per questi eventi.</value>
</data>
<data name="EnableCallIntegration" xml:space="preserve">
<value>Allega Contesto Meteo alle Chiamate</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,15 @@
<data name="AutoMessageSeverityHelp" xml:space="preserve">
<value>Alerty o tej lub wyższej ważności automatycznie wyślą wiadomość do wszystkich członków departamentu. Niższa wartość = wyższa ważność (Ekstremalna=0, Poważna=1, Umiarkowana=2, Niewielka=3).</value>
</data>
<data name="ExcludedEventsLabel" xml:space="preserve">
<value>Wykluczone zdarzenia alertów</value>
</data>
<data name="ExcludedEventsPlaceholder" xml:space="preserve">
<value>np. Ostrzeżenie o wietrze nad jeziorem, Ostrzeżenie o wietrze</value>
</data>
<data name="ExcludedEventsHelp" xml:space="preserve">
<value>Lista tytułów zdarzeń alertów oddzielonych przecinkami, dla których nie powinny być generowane automatyczne wiadomości. Alerty nadal będą wyświetlane w panelu, ale nie zostanie utworzona żadna wiadomość Resgrid dla tych zdarzeń.</value>
</data>
<data name="EnableCallIntegration" xml:space="preserve">
<value>Dołącz Kontekst Pogodowy do Wezwań</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,15 @@
<data name="AutoMessageSeverityHelp" xml:space="preserve">
<value>Varningar med denna eller högre allvarlighetsgrad skickar automatiskt ett meddelande till alla avdelningsmedlemmar. Lägre värde = högre allvarlighetsgrad (Extrem=0, Allvarlig=1, Måttlig=2, Mindre=3).</value>
</data>
<data name="ExcludedEventsLabel" xml:space="preserve">
<value>Exkluderade varningshändelser</value>
</data>
<data name="ExcludedEventsPlaceholder" xml:space="preserve">
<value>t.ex. Vindvarning vid sjö, Vindvarning</value>
</data>
<data name="ExcludedEventsHelp" xml:space="preserve">
<value>Kommaseparerad lista med varningstitlar som inte ska generera automatiska meddelanden. Varningarna visas fortfarande i instrumentpanelen men inget Resgrid-meddelande skapas för dessa händelser.</value>
</data>
<data name="EnableCallIntegration" xml:space="preserve">
<value>Bifoga Väderkontext till Utryckningar</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,15 @@
<data name="AutoMessageSeverityHelp" xml:space="preserve">
<value>Попередження з цим або вищим рівнем серйозності автоматично надішлють повідомлення всім членам підрозділу. Нижче значення = вища серйозність (Екстремальна=0, Серйозна=1, Помірна=2, Незначна=3).</value>
</data>
<data name="ExcludedEventsLabel" xml:space="preserve">
<value>Виключені події сповіщень</value>
</data>
<data name="ExcludedEventsPlaceholder" xml:space="preserve">
<value>напр. Попередження про вітер на озері, Попередження про вітер</value>
</data>
<data name="ExcludedEventsHelp" xml:space="preserve">
<value>Список назв подій сповіщень через кому, для яких не потрібно створювати автоматичні повідомлення. Сповіщення все одно відображатимуться на панелі, але повідомлення Resgrid для цих подій не буде створено.</value>
</data>
<data name="EnableCallIntegration" xml:space="preserve">
<value>Додавати Погодний Контекст до Викликів</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions Core/Resgrid.Model/DepartmentSettingTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ public enum DepartmentSettingTypes
WeatherAlertCallIntegration = 42,
WeatherAlertCacheMinutes = 43,
WeatherAlertAutoMessageSchedule = 44,
WeatherAlertExcludedEvents = 45,
}
}
3 changes: 2 additions & 1 deletion Core/Resgrid.Model/PermissionTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public enum PermissionTypes
ViewWorkflowRuns = 24,
ViewUdfFields = 25,
ManageRoutes = 26,
DeleteLog = 27
DeleteLog = 27,
UseCalendarSync = 28
}

}
20 changes: 20 additions & 0 deletions Core/Resgrid.Services/WeatherAlertService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,17 @@ public async Task SendPendingNotificationsAsync(CancellationToken ct = default)
legacyThreshold = parsed;
}

// Load excluded event titles
HashSet<string> excludedEvents = null;
var excludedSetting = await _departmentSettingsRepository.GetDepartmentSettingByIdTypeAsync(
departmentId, DepartmentSettingTypes.WeatherAlertExcludedEvents);
if (excludedSetting != null && !string.IsNullOrWhiteSpace(excludedSetting.Setting))
{
excludedEvents = new HashSet<string>(
excludedSetting.Setting.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
StringComparer.OrdinalIgnoreCase);
}

// Load department for sender info and time conversion
Department department = null;
try
Expand All @@ -353,6 +364,15 @@ public async Task SendPendingNotificationsAsync(CancellationToken ct = default)

foreach (var alert in group)
{
// Skip excluded event titles
if (excludedEvents != null && !string.IsNullOrWhiteSpace(alert.Event) && excludedEvents.Contains(alert.Event))
{
Comment on lines +368 to +369
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Trim alert.Event before exclusion lookup.

The excluded list is trimmed, but alert.Event is not. Whitespace variance can cause false non-matches.

✅ Suggested change
- if (excludedEvents != null && !string.IsNullOrWhiteSpace(alert.Event) && excludedEvents.Contains(alert.Event))
+ if (excludedEvents != null && !string.IsNullOrWhiteSpace(alert.Event) && excludedEvents.Contains(alert.Event.Trim()))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Core/Resgrid.Services/WeatherAlertService.cs` around lines 368 - 369, The
exclusion check can miss matches due to surrounding whitespace on alert.Event;
in the method using excludedEvents and alert.Event (in WeatherAlertService),
trim alert.Event before checking membership so you compare against the
already-trimmed excludedEvents (i.e., replace uses of alert.Event in the
contains check with alert.Event?.Trim() or a local var like var eventName =
alert.Event?.Trim(); and use that in the if condition and any downstream logic).

alert.NotificationSent = true;
alert.LastUpdatedUtc = DateTime.UtcNow;
await _weatherAlertRepository.UpdateAsync(alert, ct, true);
continue;
}

bool shouldSend = ShouldSendAutoMessage(alert.Severity, schedule, legacyThreshold, department);
if (shouldSend)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Resgrid.Config;
using Resgrid.Model;
using Resgrid.Model.Services;
using Resgrid.Providers.Claims;
using Resgrid.Web.Services.Helpers;
Expand All @@ -23,15 +24,27 @@ public class CalendarExportController : V4AuthenticatedApiControllerbase
private readonly ICalendarExportService _calendarExportService;
private readonly ICalendarService _calendarService;
private readonly IUserProfileService _userProfileService;
private readonly IPermissionsService _permissionsService;
private readonly IPersonnelRolesService _personnelRolesService;
private readonly IDepartmentsService _departmentsService;
private readonly IDepartmentGroupsService _departmentGroupsService;

public CalendarExportController(
ICalendarExportService calendarExportService,
ICalendarService calendarService,
IUserProfileService userProfileService)
IUserProfileService userProfileService,
IPermissionsService permissionsService,
IPersonnelRolesService personnelRolesService,
IDepartmentsService departmentsService,
IDepartmentGroupsService departmentGroupsService)
{
_calendarExportService = calendarExportService;
_calendarService = calendarService;
_userProfileService = userProfileService;
_permissionsService = permissionsService;
_personnelRolesService = personnelRolesService;
_departmentsService = departmentsService;
_departmentGroupsService = departmentGroupsService;
}

/// <summary>
Expand Down Expand Up @@ -74,6 +87,9 @@ public async Task<IActionResult> ExportDepartmentICalFeed()
[Authorize(Policy = ResgridResources.Schedule_View)]
public async Task<ActionResult<GetCalendarSubscriptionUrlResult>> GetCalendarSubscriptionUrl()
{
if (!await HasCalendarSyncPermissionAsync(DepartmentId, UserId))
return Unauthorized();

var result = new GetCalendarSubscriptionUrlResult();

// Activate if not already done; returns existing token if already active.
Expand All @@ -99,6 +115,9 @@ public async Task<ActionResult<GetCalendarSubscriptionUrlResult>> GetCalendarSub
public async Task<ActionResult<GetCalendarSubscriptionUrlResult>> RegenerateCalendarSubscriptionUrl(
CancellationToken cancellationToken)
{
if (!await HasCalendarSyncPermissionAsync(DepartmentId, UserId))
return Unauthorized();

var result = new GetCalendarSubscriptionUrlResult();

var token = await _calendarService.RegenerateCalendarSyncAsync(DepartmentId, UserId, cancellationToken);
Expand Down Expand Up @@ -132,12 +151,29 @@ public async Task<IActionResult> CalendarFeed(string token)
if (validated == null)
return Unauthorized();

if (!await HasCalendarSyncPermissionAsync(validated.Value.DepartmentId, validated.Value.UserId))
return Unauthorized();

var icsContent = await _calendarExportService.GenerateICalForDepartmentAsync(validated.Value.DepartmentId);
var bytes = Encoding.UTF8.GetBytes(icsContent);

Response.Headers["X-WR-CACHETIME"] = $"PT{CalendarConfig.ICalFeedCacheDurationMinutes}M";
return File(bytes, "text/calendar", "calendar.ics");
}

private async Task<bool> HasCalendarSyncPermissionAsync(int departmentId, string userId)
{
var permission = await _permissionsService.GetPermissionByDepartmentTypeAsync(departmentId, PermissionTypes.UseCalendarSync);
if (permission == null)
return true; // No permission configured = default Everyone

var dept = await _departmentsService.GetDepartmentByIdAsync(departmentId, false);
var isAdmin = dept != null && dept.IsUserAnAdmin(userId);
var grp = await _departmentGroupsService.GetGroupForUserAsync(userId, departmentId);
var isGroupAdmin = grp != null && grp.IsUserGroupAdmin(userId);
var roles = await _personnelRolesService.GetRolesForUserAsync(userId, departmentId);

return _permissionsService.IsUserAllowed(permission, isAdmin, isGroupAdmin, roles);
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -363,14 +363,18 @@ public async Task<ActionResult<GetWeatherAlertSettingsResult>> SaveSettings([Fro
await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, scheduleJson, DepartmentSettingTypes.WeatherAlertAutoMessageSchedule);
}

var excludedEvents = input.ExcludedEvents ?? "";
await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, excludedEvents, DepartmentSettingTypes.WeatherAlertExcludedEvents);

var result = new GetWeatherAlertSettingsResult();
result.Data = new WeatherAlertSettingsData
{
WeatherAlertsEnabled = input.WeatherAlertsEnabled,
MinimumSeverity = input.MinimumSeverity,
AutoMessageSeverity = input.AutoMessageSeverity,
CallIntegrationEnabled = input.CallIntegrationEnabled,
AutoMessageSchedule = input.AutoMessageSchedule
AutoMessageSchedule = input.AutoMessageSchedule,
ExcludedEvents = excludedEvents
};

ResponseHelper.PopulateV4ResponseData(result);
Expand All @@ -386,6 +390,7 @@ private async Task<WeatherAlertSettingsData> GetWeatherAlertSettingsDataAsync()
var autoMsgSetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSeverity);
var callIntSetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertCallIntegration);
var scheduleSetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSchedule);
var excludedEventsSetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertExcludedEvents);

if (enabledSetting != null && !string.IsNullOrWhiteSpace(enabledSetting.Setting))
settings.WeatherAlertsEnabled = bool.TryParse(enabledSetting.Setting, out var enabled) && enabled;
Expand All @@ -408,6 +413,9 @@ private async Task<WeatherAlertSettingsData> GetWeatherAlertSettingsDataAsync()
catch { }
}

if (excludedEventsSetting != null && !string.IsNullOrWhiteSpace(excludedEventsSetting.Setting))
settings.ExcludedEvents = excludedEventsSetting.Setting;

return settings;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public class SaveWeatherAlertSettingsInput
public int AutoMessageSeverity { get; set; }
public bool CallIntegrationEnabled { get; set; }
public List<WeatherAlertSeverityScheduleData> AutoMessageSchedule { get; set; }
public string ExcludedEvents { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class WeatherAlertSettingsData
public int AutoMessageSeverity { get; set; }
public bool CallIntegrationEnabled { get; set; }
public List<WeatherAlertSeverityScheduleData> AutoMessageSchedule { get; set; }
public string ExcludedEvents { get; set; }
}

public class WeatherAlertSeverityScheduleData
Expand Down
Loading
Loading