diff --git a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.ar.resx b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.ar.resx index c6ba7d56..e144592e 100644 --- a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.ar.resx +++ b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.ar.resx @@ -418,6 +418,15 @@ التنبيهات بهذه الخطورة أو أعلى سترسل تلقائياً رسالة إلى جميع أعضاء القسم. قيمة أقل = خطورة أعلى (شديدة للغاية=0، شديدة=1، معتدلة=2، طفيفة=3). + + أحداث التنبيه المستبعدة + + + مثال: تحذير رياح البحيرة، تحذير الرياح + + + قائمة مفصولة بفواصل لعناوين أحداث التنبيه التي لا ينبغي أن تولد رسائل تلقائية. ستظل التنبيهات تظهر في لوحة المعلومات ولكن لن يتم إنشاء رسالة Resgrid لهذه الأحداث. + إرفاق سياق الطقس بالمكالمات diff --git a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.de.resx b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.de.resx index fe7ead61..5e5ea3e7 100644 --- a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.de.resx +++ b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.de.resx @@ -418,6 +418,15 @@ 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). + + Ausgeschlossene Alarmereignisse + + + z.B. Windwarnung am See, Windwarnung + + + 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. + Wetterkontext an Einsätze anhängen diff --git a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.en.resx b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.en.resx index 436a8291..8b2b889e 100644 --- a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.en.resx +++ b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.en.resx @@ -418,6 +418,15 @@ 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). + + Excluded Alert Events + + + e.g. Lake Wind Advisory, Wind Advisory + + + 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. + Attach Weather Context to Calls diff --git a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.es.resx b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.es.resx index 15b51c11..072515d5 100644 --- a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.es.resx +++ b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.es.resx @@ -418,6 +418,15 @@ 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). + + Eventos de Alerta Excluidos + + + ej. Aviso de Viento en Lago, Aviso de Viento + + + 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. + Adjuntar Contexto Meteorológico a Llamadas diff --git a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.fr.resx b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.fr.resx index 88e961e8..63ca9cd3 100644 --- a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.fr.resx +++ b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.fr.resx @@ -418,6 +418,15 @@ 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). + + Événements d'alerte exclus + + + ex. Avis de vent lacustre, Avis de vent + + + 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. + Joindre le Contexte Météo aux Appels diff --git a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.it.resx b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.it.resx index fb730f94..c03231d4 100644 --- a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.it.resx +++ b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.it.resx @@ -418,6 +418,15 @@ 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). + + Eventi di allerta esclusi + + + es. Avviso vento sul lago, Avviso di vento + + + 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. + Allega Contesto Meteo alle Chiamate diff --git a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.pl.resx b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.pl.resx index 85b68980..bbeeac5e 100644 --- a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.pl.resx +++ b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.pl.resx @@ -418,6 +418,15 @@ 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). + + Wykluczone zdarzenia alertów + + + np. Ostrzeżenie o wietrze nad jeziorem, Ostrzeżenie o wietrze + + + 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ń. + Dołącz Kontekst Pogodowy do Wezwań diff --git a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.sv.resx b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.sv.resx index 27316698..d3f72c83 100644 --- a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.sv.resx +++ b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.sv.resx @@ -418,6 +418,15 @@ 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). + + Exkluderade varningshändelser + + + t.ex. Vindvarning vid sjö, Vindvarning + + + 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. + Bifoga Väderkontext till Utryckningar diff --git a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.uk.resx b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.uk.resx index 9cb6788f..4f909807 100644 --- a/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.uk.resx +++ b/Core/Resgrid.Localization/Areas/User/WeatherAlerts/WeatherAlerts.uk.resx @@ -418,6 +418,15 @@ Попередження з цим або вищим рівнем серйозності автоматично надішлють повідомлення всім членам підрозділу. Нижче значення = вища серйозність (Екстремальна=0, Серйозна=1, Помірна=2, Незначна=3). + + Виключені події сповіщень + + + напр. Попередження про вітер на озері, Попередження про вітер + + + Список назв подій сповіщень через кому, для яких не потрібно створювати автоматичні повідомлення. Сповіщення все одно відображатимуться на панелі, але повідомлення Resgrid для цих подій не буде створено. + Додавати Погодний Контекст до Викликів diff --git a/Core/Resgrid.Model/DepartmentSettingTypes.cs b/Core/Resgrid.Model/DepartmentSettingTypes.cs index 925aa6c5..788f838a 100644 --- a/Core/Resgrid.Model/DepartmentSettingTypes.cs +++ b/Core/Resgrid.Model/DepartmentSettingTypes.cs @@ -46,5 +46,6 @@ public enum DepartmentSettingTypes WeatherAlertCallIntegration = 42, WeatherAlertCacheMinutes = 43, WeatherAlertAutoMessageSchedule = 44, + WeatherAlertExcludedEvents = 45, } } diff --git a/Core/Resgrid.Model/PermissionTypes.cs b/Core/Resgrid.Model/PermissionTypes.cs index 254f3afe..e2029d68 100644 --- a/Core/Resgrid.Model/PermissionTypes.cs +++ b/Core/Resgrid.Model/PermissionTypes.cs @@ -29,7 +29,8 @@ public enum PermissionTypes ViewWorkflowRuns = 24, ViewUdfFields = 25, ManageRoutes = 26, - DeleteLog = 27 + DeleteLog = 27, + UseCalendarSync = 28 } } diff --git a/Core/Resgrid.Services/WeatherAlertService.cs b/Core/Resgrid.Services/WeatherAlertService.cs index 7ad101c4..8ac41945 100644 --- a/Core/Resgrid.Services/WeatherAlertService.cs +++ b/Core/Resgrid.Services/WeatherAlertService.cs @@ -340,6 +340,17 @@ public async Task SendPendingNotificationsAsync(CancellationToken ct = default) legacyThreshold = parsed; } + // Load excluded event titles + HashSet excludedEvents = null; + var excludedSetting = await _departmentSettingsRepository.GetDepartmentSettingByIdTypeAsync( + departmentId, DepartmentSettingTypes.WeatherAlertExcludedEvents); + if (excludedSetting != null && !string.IsNullOrWhiteSpace(excludedSetting.Setting)) + { + excludedEvents = new HashSet( + excludedSetting.Setting.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), + StringComparer.OrdinalIgnoreCase); + } + // Load department for sender info and time conversion Department department = null; try @@ -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)) + { + alert.NotificationSent = true; + alert.LastUpdatedUtc = DateTime.UtcNow; + await _weatherAlertRepository.UpdateAsync(alert, ct, true); + continue; + } + bool shouldSend = ShouldSendAutoMessage(alert.Severity, schedule, legacyThreshold, department); if (shouldSend) { diff --git a/Web/Resgrid.Web.Services/Controllers/v4/CalendarExportController.cs b/Web/Resgrid.Web.Services/Controllers/v4/CalendarExportController.cs index b2dcb281..bb7f6288 100644 --- a/Web/Resgrid.Web.Services/Controllers/v4/CalendarExportController.cs +++ b/Web/Resgrid.Web.Services/Controllers/v4/CalendarExportController.cs @@ -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; @@ -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; } /// @@ -74,6 +87,9 @@ public async Task ExportDepartmentICalFeed() [Authorize(Policy = ResgridResources.Schedule_View)] public async Task> GetCalendarSubscriptionUrl() { + if (!await HasCalendarSyncPermissionAsync(DepartmentId, UserId)) + return Unauthorized(); + var result = new GetCalendarSubscriptionUrlResult(); // Activate if not already done; returns existing token if already active. @@ -99,6 +115,9 @@ public async Task> GetCalendarSub public async Task> RegenerateCalendarSubscriptionUrl( CancellationToken cancellationToken) { + if (!await HasCalendarSyncPermissionAsync(DepartmentId, UserId)) + return Unauthorized(); + var result = new GetCalendarSubscriptionUrlResult(); var token = await _calendarService.RegenerateCalendarSyncAsync(DepartmentId, UserId, cancellationToken); @@ -132,12 +151,29 @@ public async Task 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 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); + } } } - diff --git a/Web/Resgrid.Web.Services/Controllers/v4/WeatherAlertsController.cs b/Web/Resgrid.Web.Services/Controllers/v4/WeatherAlertsController.cs index 2921e89a..dafd8c61 100644 --- a/Web/Resgrid.Web.Services/Controllers/v4/WeatherAlertsController.cs +++ b/Web/Resgrid.Web.Services/Controllers/v4/WeatherAlertsController.cs @@ -363,6 +363,9 @@ public async Task> 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 { @@ -370,7 +373,8 @@ public async Task> SaveSettings([Fro MinimumSeverity = input.MinimumSeverity, AutoMessageSeverity = input.AutoMessageSeverity, CallIntegrationEnabled = input.CallIntegrationEnabled, - AutoMessageSchedule = input.AutoMessageSchedule + AutoMessageSchedule = input.AutoMessageSchedule, + ExcludedEvents = excludedEvents }; ResponseHelper.PopulateV4ResponseData(result); @@ -386,6 +390,7 @@ private async Task 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; @@ -408,6 +413,9 @@ private async Task GetWeatherAlertSettingsDataAsync() catch { } } + if (excludedEventsSetting != null && !string.IsNullOrWhiteSpace(excludedEventsSetting.Setting)) + settings.ExcludedEvents = excludedEventsSetting.Setting; + return settings; } diff --git a/Web/Resgrid.Web.Services/Models/v4/WeatherAlerts/SaveWeatherAlertSettingsInput.cs b/Web/Resgrid.Web.Services/Models/v4/WeatherAlerts/SaveWeatherAlertSettingsInput.cs index fba8b462..2a6fd57e 100644 --- a/Web/Resgrid.Web.Services/Models/v4/WeatherAlerts/SaveWeatherAlertSettingsInput.cs +++ b/Web/Resgrid.Web.Services/Models/v4/WeatherAlerts/SaveWeatherAlertSettingsInput.cs @@ -9,5 +9,6 @@ public class SaveWeatherAlertSettingsInput public int AutoMessageSeverity { get; set; } public bool CallIntegrationEnabled { get; set; } public List AutoMessageSchedule { get; set; } + public string ExcludedEvents { get; set; } } } diff --git a/Web/Resgrid.Web.Services/Models/v4/WeatherAlerts/WeatherAlertSettingsData.cs b/Web/Resgrid.Web.Services/Models/v4/WeatherAlerts/WeatherAlertSettingsData.cs index 4975d8ff..c1608a66 100644 --- a/Web/Resgrid.Web.Services/Models/v4/WeatherAlerts/WeatherAlertSettingsData.cs +++ b/Web/Resgrid.Web.Services/Models/v4/WeatherAlerts/WeatherAlertSettingsData.cs @@ -9,6 +9,7 @@ public class WeatherAlertSettingsData public int AutoMessageSeverity { get; set; } public bool CallIntegrationEnabled { get; set; } public List AutoMessageSchedule { get; set; } + public string ExcludedEvents { get; set; } } public class WeatherAlertSeverityScheduleData diff --git a/Web/Resgrid.Web/Areas/User/Controllers/CalendarController.cs b/Web/Resgrid.Web/Areas/User/Controllers/CalendarController.cs index 07b0d391..b97d73bb 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/CalendarController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/CalendarController.cs @@ -34,10 +34,13 @@ public class CalendarController : SecureBaseController private readonly IEventAggregator _eventAggregator; private readonly IAuthorizationService _authorizationService; private readonly IUserProfileService _userProfileService; + private readonly IPermissionsService _permissionsService; + private readonly IPersonnelRolesService _personnelRolesService; public CalendarController(IDepartmentsService departmentsService, IUsersService usersService, ICalendarService calendarService, IDepartmentGroupsService departmentGroupsService, IGeoLocationProvider geoLocationProvider, IEventAggregator eventAggregator, - IAuthorizationService authorizationService, IUserProfileService userProfileService) + IAuthorizationService authorizationService, IUserProfileService userProfileService, + IPermissionsService permissionsService, IPersonnelRolesService personnelRolesService) { _departmentsService = departmentsService; _usersService = usersService; @@ -47,6 +50,8 @@ public CalendarController(IDepartmentsService departmentsService, IUsersService _eventAggregator = eventAggregator; _authorizationService = authorizationService; _userProfileService = userProfileService; + _permissionsService = permissionsService; + _personnelRolesService = personnelRolesService; } #endregion Private Members and Constructors @@ -69,13 +74,25 @@ public async Task Index() model.UpcomingItems = new List(); model.UpcomingItems = await _calendarService.GetUpcomingCalendarItemsAsync(DepartmentId, DateTime.UtcNow); + // Check calendar sync permission + var calSyncPermission = await _permissionsService.GetPermissionByDepartmentTypeAsync(DepartmentId, PermissionTypes.UseCalendarSync); + var department = model.Department; + var isAdmin = department.IsUserAnAdmin(UserId); + var group = await _departmentGroupsService.GetGroupForUserAsync(UserId, DepartmentId); + var isGroupAdmin = group != null && group.IsUserGroupAdmin(UserId); + var roles = await _personnelRolesService.GetRolesForUserAsync(UserId, DepartmentId); + model.CanUseCalendarSync = _permissionsService.IsUserAllowed(calSyncPermission, isAdmin, isGroupAdmin, roles); + // Populate calendar sync token for the subscribe panel. - var profile = await _userProfileService.GetProfileByUserIdAsync(UserId); - if (profile != null && !String.IsNullOrWhiteSpace(profile.CalendarSyncToken)) + if (model.CanUseCalendarSync) { - model.CalendarSyncToken = profile.CalendarSyncToken; - var feedToken = await _calendarService.GetCalendarFeedTokenAsync(DepartmentId, UserId); - model.CalendarSubscriptionUrl = $"{SystemBehaviorConfig.ResgridApiBaseUrl}/api/v4/CalendarExport/CalendarFeed/{feedToken}"; + var profile = await _userProfileService.GetProfileByUserIdAsync(UserId); + if (profile != null && !String.IsNullOrWhiteSpace(profile.CalendarSyncToken)) + { + model.CalendarSyncToken = profile.CalendarSyncToken; + var feedToken = await _calendarService.GetCalendarFeedTokenAsync(DepartmentId, UserId); + model.CalendarSubscriptionUrl = $"{SystemBehaviorConfig.ResgridApiBaseUrl}/api/v4/CalendarExport/CalendarFeed/{feedToken}"; + } } return View(model); @@ -895,6 +912,13 @@ public async Task GetMapDataForItem(int calendarItemId) [ValidateAntiForgeryToken] public async Task ActivateCalendarSync(CancellationToken cancellationToken) { + var permission = await _permissionsService.GetPermissionByDepartmentTypeAsync(DepartmentId, PermissionTypes.UseCalendarSync); + var dept = await _departmentsService.GetDepartmentByIdAsync(DepartmentId, false); + var grp = await _departmentGroupsService.GetGroupForUserAsync(UserId, DepartmentId); + var roles = await _personnelRolesService.GetRolesForUserAsync(UserId, DepartmentId); + if (!_permissionsService.IsUserAllowed(permission, dept.IsUserAnAdmin(UserId), grp != null && grp.IsUserGroupAdmin(UserId), roles)) + return Unauthorized(); + await _calendarService.ActivateCalendarSyncAsync(DepartmentId, UserId, cancellationToken); return RedirectToAction("Index"); } @@ -908,10 +932,41 @@ public async Task ActivateCalendarSync(CancellationToken cancella [ValidateAntiForgeryToken] public async Task RegenerateCalendarSync(CancellationToken cancellationToken) { + var permission = await _permissionsService.GetPermissionByDepartmentTypeAsync(DepartmentId, PermissionTypes.UseCalendarSync); + var dept = await _departmentsService.GetDepartmentByIdAsync(DepartmentId, false); + var grp = await _departmentGroupsService.GetGroupForUserAsync(UserId, DepartmentId); + var roles = await _personnelRolesService.GetRolesForUserAsync(UserId, DepartmentId); + if (!_permissionsService.IsUserAllowed(permission, dept.IsUserAnAdmin(UserId), grp != null && grp.IsUserGroupAdmin(UserId), roles)) + return Unauthorized(); + await _calendarService.RegenerateCalendarSyncAsync(DepartmentId, UserId, cancellationToken); return RedirectToAction("Index"); } + /// + /// Admin action: regenerates calendar sync tokens for ALL users in the department who have one provisioned. + /// + [HttpPost] + [Authorize(Policy = ResgridResources.Department_Update)] + [ValidateAntiForgeryToken] + public async Task RegenerateAllCalendarSyncTokens(CancellationToken cancellationToken) + { + var members = await _departmentsService.GetAllMembersForDepartmentAsync(DepartmentId); + if (members != null) + { + foreach (var member in members) + { + var profile = await _userProfileService.GetProfileByUserIdAsync(member.UserId); + if (profile != null && !string.IsNullOrWhiteSpace(profile.CalendarSyncToken)) + { + await _calendarService.RegenerateCalendarSyncAsync(DepartmentId, member.UserId, cancellationToken); + } + } + } + + return RedirectToAction("Api", "Department"); + } + // -- Check-In Attendance ------------------------------------------------------- [HttpPost] diff --git a/Web/Resgrid.Web/Areas/User/Controllers/CommunicationTestController.cs b/Web/Resgrid.Web/Areas/User/Controllers/CommunicationTestController.cs index e11b2ce4..9501933f 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/CommunicationTestController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/CommunicationTestController.cs @@ -22,24 +22,40 @@ public class CommunicationTestController : Resgrid.Web.SecureBaseController private readonly ICommunicationTestService _communicationTestService; private readonly IUserProfileService _userProfileService; private readonly IEventAggregator _eventAggregator; + private readonly IDepartmentGroupsService _departmentGroupsService; + private readonly IDepartmentsService _departmentsService; public CommunicationTestController( ICommunicationTestService communicationTestService, IUserProfileService userProfileService, - IEventAggregator eventAggregator) + IEventAggregator eventAggregator, + IDepartmentGroupsService departmentGroupsService, + IDepartmentsService departmentsService) { _communicationTestService = communicationTestService; _userProfileService = userProfileService; _eventAggregator = eventAggregator; + _departmentGroupsService = departmentGroupsService; + _departmentsService = departmentsService; } [HttpGet] public async Task Index() { - if (!ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) + bool isDeptAdmin = ClaimsAuthorizationHelper.IsUserDepartmentAdmin(); + bool isGroupAdmin = false; + + if (!isDeptAdmin) + { + var group = await _departmentGroupsService.GetGroupForUserAsync(UserId, DepartmentId); + isGroupAdmin = group != null && group.IsUserGroupAdmin(UserId); + } + + if (!isDeptAdmin && !isGroupAdmin) return Unauthorized(); var model = new CommunicationTestIndexView(); + model.IsDepartmentAdmin = isDeptAdmin; var tests = await _communicationTestService.GetTestsByDepartmentIdAsync(DepartmentId); if (tests != null) @@ -287,7 +303,17 @@ public async Task StartRun(string testId, CancellationToken cance [HttpGet] public async Task Report(string runId) { - if (!ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) + bool isDeptAdmin = ClaimsAuthorizationHelper.IsUserDepartmentAdmin(); + bool isGroupAdmin = false; + DepartmentGroup userGroup = null; + + if (!isDeptAdmin) + { + userGroup = await _departmentGroupsService.GetGroupForUserAsync(UserId, DepartmentId); + isGroupAdmin = userGroup != null && userGroup.IsUserGroupAdmin(UserId); + } + + if (!isDeptAdmin && !isGroupAdmin) return Unauthorized(); if (!Guid.TryParse(runId, out var id)) @@ -299,13 +325,39 @@ public async Task Report(string runId) var test = await _communicationTestService.GetTestByIdAsync(run.CommunicationTestId); var results = await _communicationTestService.GetResultsByRunIdAsync(id); + var resultList = results?.ToList() ?? new List(); var profiles = await _userProfileService.GetAllProfilesForDepartmentAsync(DepartmentId); + // Group admins only see results for members in their group and child groups + if (!isDeptAdmin && isGroupAdmin && userGroup != null) + { + var allowedUserIds = new HashSet(); + + // Add members of the admin's own group + var groupMembers = await _departmentGroupsService.GetAllMembersForGroupAsync(userGroup.DepartmentGroupId); + foreach (var m in groupMembers) + allowedUserIds.Add(m.UserId); + + // Add members of child groups + var childGroups = await _departmentGroupsService.GetAllChildDepartmentGroupsAsync(userGroup.DepartmentGroupId); + if (childGroups != null) + { + foreach (var childGroup in childGroups) + { + var childMembers = await _departmentGroupsService.GetAllMembersForGroupAsync(childGroup.DepartmentGroupId); + foreach (var m in childMembers) + allowedUserIds.Add(m.UserId); + } + } + + resultList = resultList.Where(r => allowedUserIds.Contains(r.UserId)).ToList(); + } + var model = new CommunicationTestReportView { Run = run, Test = test, - Results = results?.ToList() ?? new List(), + Results = resultList, Profiles = profiles ?? new Dictionary() }; diff --git a/Web/Resgrid.Web/Areas/User/Controllers/SecurityController.cs b/Web/Resgrid.Web/Areas/User/Controllers/SecurityController.cs index d1802ed1..f5e334d1 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/SecurityController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/SecurityController.cs @@ -382,6 +382,19 @@ public async Task Index() model.ViewWorkflowRunsPermissions = new SelectList(viewWorkflowRunsPermissions, "Id", "Name"); // 2FA enforcement scope � only managingUser can change this + if (permissions.Any(x => x.PermissionType == (int)PermissionTypes.UseCalendarSync)) + model.UseCalendarSync = permissions.First(x => x.PermissionType == (int)PermissionTypes.UseCalendarSync).Action; + else + model.UseCalendarSync = 3; + + var useCalendarSyncPermissions = new List(); + useCalendarSyncPermissions.Add(new { Id = 3, Name = "Everyone" }); + useCalendarSyncPermissions.Add(new { Id = 0, Name = "Department Admins" }); + useCalendarSyncPermissions.Add(new { Id = 1, Name = "Department and Group Admins" }); + useCalendarSyncPermissions.Add(new { Id = 2, Name = "Department Admins and Select Roles" }); + model.UseCalendarSyncPermissions = new SelectList(useCalendarSyncPermissions, "Id", "Name"); + + var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); model.IsManagingUser = department.ManagingUserId == UserId; model.Require2FAForAdmins = await _departmentSettingsService.GetRequire2FAForAdminsAsync(DepartmentId); diff --git a/Web/Resgrid.Web/Areas/User/Models/Calendar/IndexView.cs b/Web/Resgrid.Web/Areas/User/Models/Calendar/IndexView.cs index a1368eb8..16d40245 100644 --- a/Web/Resgrid.Web/Areas/User/Models/Calendar/IndexView.cs +++ b/Web/Resgrid.Web/Areas/User/Models/Calendar/IndexView.cs @@ -14,5 +14,7 @@ public class IndexView public string CalendarSyncToken { get; set; } /// The full HTTPS subscription URL to display to the user once sync is activated. public string CalendarSubscriptionUrl { get; set; } + /// Whether the current user has permission to use calendar sync. + public bool CanUseCalendarSync { get; set; } } } diff --git a/Web/Resgrid.Web/Areas/User/Models/CommunicationTests/CommunicationTestIndexView.cs b/Web/Resgrid.Web/Areas/User/Models/CommunicationTests/CommunicationTestIndexView.cs index ecc923d0..644c1d50 100644 --- a/Web/Resgrid.Web/Areas/User/Models/CommunicationTests/CommunicationTestIndexView.cs +++ b/Web/Resgrid.Web/Areas/User/Models/CommunicationTests/CommunicationTestIndexView.cs @@ -8,5 +8,6 @@ public class CommunicationTestIndexView : BaseUserModel public List Tests { get; set; } = new List(); public List RecentRuns { get; set; } = new List(); public Dictionary TestNames { get; set; } = new Dictionary(); + public bool IsDepartmentAdmin { get; set; } } } diff --git a/Web/Resgrid.Web/Areas/User/Models/Security/PermissionsView.cs b/Web/Resgrid.Web/Areas/User/Models/Security/PermissionsView.cs index 17f38f24..684662ce 100644 --- a/Web/Resgrid.Web/Areas/User/Models/Security/PermissionsView.cs +++ b/Web/Resgrid.Web/Areas/User/Models/Security/PermissionsView.cs @@ -91,6 +91,9 @@ public class PermissionsView public int ViewWorkflowRuns { get; set; } public SelectList ViewWorkflowRunsPermissions { get; set; } + public int UseCalendarSync { get; set; } + public SelectList UseCalendarSyncPermissions { get; set; } + // Two-Factor Authentication enforcement public int Require2FAForAdmins { get; set; } public SelectList Require2FAForAdminsOptions { get; set; } diff --git a/Web/Resgrid.Web/Areas/User/Views/Calendar/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Calendar/Index.cshtml index d9d3149d..245ea25b 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Calendar/Index.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Calendar/Index.cshtml @@ -134,6 +134,8 @@ @* Calendar Sync Panel *@ + @if (Model.CanUseCalendarSync) + {
@localizer["CalendarSyncTitle"]
@@ -191,9 +193,12 @@ @localizer["RegenerateCalendarSync"] + + }
+ } diff --git a/Web/Resgrid.Web/Areas/User/Views/CommunicationTest/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/CommunicationTest/Index.cshtml index 7156c42e..20f32cfc 100644 --- a/Web/Resgrid.Web/Areas/User/Views/CommunicationTest/Index.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/CommunicationTest/Index.cshtml @@ -72,20 +72,23 @@ @test.ResponseWindowMinutes min - @if (test.ScheduleType == (int)Resgrid.Model.CommunicationTestScheduleType.OnDemand) + @if (Model.IsDepartmentAdmin) { -
+ if (test.ScheduleType == (int)Resgrid.Model.CommunicationTestScheduleType.OnDemand) + { + + @Html.AntiForgeryToken() + + +
+ } + Edit +
@Html.AntiForgeryToken() - +
} - Edit -
- @Html.AntiForgeryToken() - - -
} diff --git a/Web/Resgrid.Web/Areas/User/Views/Department/Api.cshtml b/Web/Resgrid.Web/Areas/User/Views/Department/Api.cshtml index f57ff70c..ce34f850 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Department/Api.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Department/Api.cshtml @@ -58,6 +58,31 @@ + +
+
+
+
+
+
Calendar Sync Tokens
+

+ Calendar sync allows users to subscribe to the department calendar from external calendar applications (Google Calendar, Outlook, Apple Calendar). + Use the button below to regenerate all calendar sync tokens for every user in the department. This will invalidate all existing subscription URLs + and users will need to re-subscribe. +

+
+ @Html.AntiForgeryToken() + +
+
+
+
+
+
+ @section Scripts { diff --git a/Web/Resgrid.Web/Areas/User/Views/Security/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Security/Index.cshtml index e73bb737..5e07c43c 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Security/Index.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Security/Index.cshtml @@ -313,6 +313,16 @@ + + Use Calendar Sync + Controls who can activate and use calendar subscription URLs to sync Resgrid calendar events to external calendar applications. + @Html.DropDownListFor(m => m.UseCalendarSync, Model.UseCalendarSyncPermissions) + @localizer["PermissionNA"] + + @localizer["PermissionNoRoles"] + + + diff --git a/Web/Resgrid.Web/Areas/User/Views/WeatherAlerts/Settings.cshtml b/Web/Resgrid.Web/Areas/User/Views/WeatherAlerts/Settings.cshtml index cc2f6f37..efc76f5c 100644 --- a/Web/Resgrid.Web/Areas/User/Views/WeatherAlerts/Settings.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/WeatherAlerts/Settings.cshtml @@ -87,6 +87,13 @@ +
+ +
+ + @localizer["ExcludedEventsHelp"] +
+
@@ -328,6 +335,8 @@ $('.sched-end[data-severity="' + entry.Severity + '"]').val(entry.EndHour); }); } + + $('#settingExcludedEvents').val(data.Data.ExcludedEvents || ''); } }, error: function () { @@ -353,7 +362,8 @@ MinimumSeverity: parseInt($('#settingMinSeverity').val()), AutoMessageSeverity: parseInt($('#settingAutoMsgSeverity').val()), CallIntegrationEnabled: $('#settingCallIntegration').is(':checked'), - AutoMessageSchedule: schedule + AutoMessageSchedule: schedule, + ExcludedEvents: $('#settingExcludedEvents').val() || '' }; $.ajax({ diff --git a/Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js b/Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js index 2d5c3120..04c166e5 100644 --- a/Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js +++ b/Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js @@ -755,6 +755,33 @@ var resgrid; initPermRoles("#workflowRunsRoles", 24); //////////////////////////////////////////////////////// + // Use Calendar Sync + //////////////////////////////////////////////////////// + $('#UseCalendarSync').change(function () { + var val = this.value; + $.ajax({ + url: resgrid.absoluteBaseUrl + '/User/Security/SetPermission?type=28&perm=' + val, + type: 'GET' + }).done(function (results) { + }); + if ($("#UseCalendarSync").val() === "2") { + $('#calSyncNoRolesSpan').hide(); + $('#calSyncRolesDiv').show(); + } else { + $('#calSyncNoRolesSpan').show(); + $('#calSyncRolesDiv').hide(); + } + }); + if ($("#UseCalendarSync").val() === "2") { + $('#calSyncNoRolesSpan').hide(); + $('#calSyncRolesDiv').show(); + } else { + $('#calSyncNoRolesSpan').show(); + $('#calSyncRolesDiv').hide(); + } + initPermRoles("#calSyncRoles", 28); + //////////////////////////////////////////////////////// + }); })(permissions = security.permissions || (security.permissions = {})); })(security = resgrid.security || (resgrid.security = {}));