diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx index c50c8844..d9dcdfd3 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx @@ -157,4 +157,31 @@ المدة (دقائق) حد التحذير (دقائق) المصدر + بث الفيديو + إضافة بث فيديو + تعديل بث الفيديو + حذف بث الفيديو + الاسم + الرابط + نوع البث + تنسيق البث + الوصف + الحالة + الموقع + أضيف بواسطة + تاريخ الإضافة + ترتيب الفرز + نشط + غير نشط + خطأ + طائرة مسيّرة + كاميرا ثابتة + كاميرا الجسم + كاميرا المرور + كاميرا الطقس + بث فضائي + كاميرا ويب + أخرى + هل أنت متأكد من رغبتك في حذف بث الفيديو هذا؟ + لم تتم إضافة أي بث فيديو لهذه المكالمة. diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx index 7851beef..7b63023d 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx @@ -524,4 +524,85 @@ Quelle + + Videoübertragungen + + + Videoübertragung hinzufügen + + + Videoübertragung bearbeiten + + + Videoübertragung löschen + + + Name + + + URL + + + Übertragungstyp + + + Übertragungsformat + + + Beschreibung + + + Status + + + Standort + + + Hinzugefügt von + + + Hinzugefügt am + + + Sortierreihenfolge + + + Aktiv + + + Inaktiv + + + Fehler + + + Drohne + + + Feste Kamera + + + Körperkamera + + + Verkehrskamera + + + Wetterkamera + + + Satellitenübertragung + + + Webkamera + + + Sonstiges + + + Sind Sie sicher, dass Sie diese Videoübertragung löschen möchten? + + + Keine Videoübertragungen für diesen Einsatz hinzugefügt. + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx index edf9c74c..bdc1ac40 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx @@ -573,4 +573,85 @@ Source + + Video Feeds + + + Add Video Feed + + + Edit Video Feed + + + Delete Video Feed + + + Name + + + URL + + + Feed Type + + + Feed Format + + + Description + + + Status + + + Location + + + Added By + + + Added On + + + Sort Order + + + Active + + + Inactive + + + Error + + + Drone + + + Fixed Camera + + + Body Cam + + + Traffic Cam + + + Weather Cam + + + Satellite Feed + + + Web Cam + + + Other + + + Are you sure you want to delete this video feed? + + + No video feeds have been added to this call. + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx index 2e8bbc40..30a77482 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx @@ -573,4 +573,85 @@ Fuente + + Transmisiones de Video + + + Agregar Transmisión de Video + + + Editar Transmisión de Video + + + Eliminar Transmisión de Video + + + Nombre + + + URL + + + Tipo de Transmisión + + + Formato de Transmisión + + + Descripción + + + Estado + + + Ubicación + + + Agregado Por + + + Fecha de Agregado + + + Orden + + + Activo + + + Inactivo + + + Error + + + Dron + + + Cámara Fija + + + Cámara Corporal + + + Cámara de Tráfico + + + Cámara Meteorológica + + + Transmisión Satelital + + + Cámara Web + + + Otro + + + ¿Está seguro de que desea eliminar esta transmisión de video? + + + No se han agregado transmisiones de video a esta llamada. + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx index 8b4ee8f6..fe5b10e2 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx @@ -524,4 +524,85 @@ Source + + Flux Vidéo + + + Ajouter un Flux Vidéo + + + Modifier le Flux Vidéo + + + Supprimer le Flux Vidéo + + + Nom + + + URL + + + Type de Flux + + + Format de Flux + + + Description + + + Statut + + + Emplacement + + + Ajouté par + + + Ajouté le + + + Ordre de tri + + + Actif + + + Inactif + + + Erreur + + + Drone + + + Caméra Fixe + + + Caméra Corporelle + + + Caméra de Circulation + + + Caméra Météo + + + Flux Satellite + + + Webcam + + + Autre + + + Êtes-vous sûr de vouloir supprimer ce flux vidéo ? + + + Aucun flux vidéo n'a été ajouté à cet appel. + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx index 12f4f3d9..cbc6668d 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx @@ -524,4 +524,85 @@ Fonte + + Feed Video + + + Aggiungi Feed Video + + + Modifica Feed Video + + + Elimina Feed Video + + + Nome + + + URL + + + Tipo di Feed + + + Formato Feed + + + Descrizione + + + Stato + + + Posizione + + + Aggiunto da + + + Aggiunto il + + + Ordinamento + + + Attivo + + + Inattivo + + + Errore + + + Drone + + + Telecamera Fissa + + + Telecamera Corporea + + + Telecamera Traffico + + + Telecamera Meteo + + + Feed Satellitare + + + Webcam + + + Altro + + + Sei sicuro di voler eliminare questo feed video? + + + Nessun feed video è stato aggiunto a questa chiamata. + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx index 56b2c540..183b59d5 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx @@ -524,4 +524,85 @@ Źródło + + Transmisje Wideo + + + Dodaj Transmisję Wideo + + + Edytuj Transmisję Wideo + + + Usuń Transmisję Wideo + + + Nazwa + + + URL + + + Typ Transmisji + + + Format Transmisji + + + Opis + + + Status + + + Lokalizacja + + + Dodane przez + + + Data dodania + + + Kolejność + + + Aktywny + + + Nieaktywny + + + Błąd + + + Dron + + + Kamera Stała + + + Kamera Osobista + + + Kamera Drogowa + + + Kamera Pogodowa + + + Transmisja Satelitarna + + + Kamera Internetowa + + + Inne + + + Czy na pewno chcesz usunąć tę transmisję wideo? + + + Nie dodano żadnych transmisji wideo do tego zgłoszenia. + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx index 99ddf435..44f4cc44 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx @@ -524,4 +524,85 @@ Källa + + Videoflöden + + + Lägg till Videoflöde + + + Redigera Videoflöde + + + Ta bort Videoflöde + + + Namn + + + URL + + + Flödestyp + + + Flödesformat + + + Beskrivning + + + Status + + + Plats + + + Tillagd av + + + Tillagd den + + + Sorteringsordning + + + Aktiv + + + Inaktiv + + + Fel + + + Drönare + + + Fast Kamera + + + Kroppskamera + + + Trafikkamera + + + Väderkamera + + + Satellitflöde + + + Webbkamera + + + Övrigt + + + Är du säker på att du vill ta bort detta videoflöde? + + + Inga videoflöden har lagts till för detta ärende. + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx index a513d5c1..efec086b 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx @@ -524,4 +524,85 @@ Джерело + + Відеотрансляції + + + Додати Відеотрансляцію + + + Редагувати Відеотрансляцію + + + Видалити Відеотрансляцію + + + Назва + + + URL + + + Тип Трансляції + + + Формат Трансляції + + + Опис + + + Статус + + + Місцезнаходження + + + Додано + + + Дата додавання + + + Порядок сортування + + + Активний + + + Неактивний + + + Помилка + + + Дрон + + + Стаціонарна Камера + + + Натільна Камера + + + Камера Дорожнього Руху + + + Метеокамера + + + Супутникова Трансляція + + + Веб-камера + + + Інше + + + Ви впевнені, що хочете видалити цю відеотрансляцію? + + + До цього виклику не додано жодних відеотрансляцій. + diff --git a/Core/Resgrid.Model/Call.cs b/Core/Resgrid.Model/Call.cs index 7a843eb3..0bf24acd 100644 --- a/Core/Resgrid.Model/Call.cs +++ b/Core/Resgrid.Model/Call.cs @@ -139,6 +139,9 @@ public class Call : IEntity [ProtoMember(32)] public virtual ICollection Contacts { get; set; } + [ProtoMember(33)] + public virtual ICollection VideoFeeds { get; set; } + public string ContactName { get; set; } public string ContactNumber { get; set; } @@ -199,7 +202,7 @@ public object IdValue public int IdType => 0; [NotMapped] - public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "ReportingUser", "ClosedByUser", "Department", "Dispatches", "Attachments", "CallNotes", "GroupDispatches", "UnitDispatches", "RoleDispatches", "Protocols", "ShortenedAudioUrl", "ShortenedCallUrl", "CallPriority", "PreviousDispatchCount", "References", "Contacts" }; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "ReportingUser", "ClosedByUser", "Department", "Dispatches", "Attachments", "CallNotes", "GroupDispatches", "UnitDispatches", "RoleDispatches", "Protocols", "ShortenedAudioUrl", "ShortenedCallUrl", "CallPriority", "PreviousDispatchCount", "References", "Contacts", "VideoFeeds" }; public string GetIdentifier() { diff --git a/Core/Resgrid.Model/CallVideoFeed.cs b/Core/Resgrid.Model/CallVideoFeed.cs new file mode 100644 index 00000000..23c10a10 --- /dev/null +++ b/Core/Resgrid.Model/CallVideoFeed.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; +using Resgrid.Framework; + +namespace Resgrid.Model +{ + [Table("CallVideoFeeds")] + public class CallVideoFeed : IEntity + { + public string CallVideoFeedId { get; set; } + + public int CallId { get; set; } + + public int DepartmentId { get; set; } + + [Required] + public string Name { get; set; } + + [Required] + public string Url { get; set; } + + public int? FeedType { get; set; } + + public int? FeedFormat { get; set; } + + public string Description { get; set; } + + public int Status { get; set; } + + [DecimalPrecision(10, 7)] + public decimal? Latitude { get; set; } + + [DecimalPrecision(10, 7)] + public decimal? Longitude { get; set; } + + public string AddedByUserId { get; set; } + + public DateTime AddedOn { get; set; } + + public DateTime? UpdatedOn { get; set; } + + public int SortOrder { get; set; } + + public bool IsDeleted { get; set; } + + public string DeletedByUserId { get; set; } + + public DateTime? DeletedOn { get; set; } + + public bool IsFlagged { get; set; } + + public string FlaggedReason { get; set; } + + public string FlaggedByUserId { get; set; } + + public DateTime? FlaggedOn { get; set; } + + [ForeignKey("CallId")] + public virtual Call Call { get; set; } + + [NotMapped] + public string TableName => "CallVideoFeeds"; + + [NotMapped] + public string IdName => "CallVideoFeedId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return CallVideoFeedId; } + set { CallVideoFeedId = (string)value; } + } + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "Call" }; + } +} diff --git a/Core/Resgrid.Model/CallVideoFeedFormats.cs b/Core/Resgrid.Model/CallVideoFeedFormats.cs new file mode 100644 index 00000000..c3261ca0 --- /dev/null +++ b/Core/Resgrid.Model/CallVideoFeedFormats.cs @@ -0,0 +1,14 @@ +namespace Resgrid.Model +{ + public enum CallVideoFeedFormats + { + RTSP = 0, + HLS = 1, + MJPEG = 2, + YouTubeLive = 3, + WebRTC = 4, + DASH = 5, + Embed = 6, + Other = 99 + } +} diff --git a/Core/Resgrid.Model/CallVideoFeedStatuses.cs b/Core/Resgrid.Model/CallVideoFeedStatuses.cs new file mode 100644 index 00000000..a5ebd910 --- /dev/null +++ b/Core/Resgrid.Model/CallVideoFeedStatuses.cs @@ -0,0 +1,9 @@ +namespace Resgrid.Model +{ + public enum CallVideoFeedStatuses + { + Active = 0, + Inactive = 1, + Error = 2 + } +} diff --git a/Core/Resgrid.Model/CallVideoFeedTypes.cs b/Core/Resgrid.Model/CallVideoFeedTypes.cs new file mode 100644 index 00000000..59b34ada --- /dev/null +++ b/Core/Resgrid.Model/CallVideoFeedTypes.cs @@ -0,0 +1,14 @@ +namespace Resgrid.Model +{ + public enum CallVideoFeedTypes + { + Drone = 0, + FixedCamera = 1, + BodyCam = 2, + TrafficCam = 3, + WeatherCam = 4, + SatelliteFeed = 5, + WebCam = 6, + Other = 99 + } +} diff --git a/Core/Resgrid.Model/Repositories/ICallVideoFeedRepository.cs b/Core/Resgrid.Model/Repositories/ICallVideoFeedRepository.cs new file mode 100644 index 00000000..ca8b917c --- /dev/null +++ b/Core/Resgrid.Model/Repositories/ICallVideoFeedRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + public interface ICallVideoFeedRepository : IRepository + { + Task> GetByCallIdAsync(int callId); + Task> GetByDepartmentIdAsync(int departmentId); + } +} diff --git a/Core/Resgrid.Model/Services/ICallsService.cs b/Core/Resgrid.Model/Services/ICallsService.cs index b47154b2..ae82eadd 100644 --- a/Core/Resgrid.Model/Services/ICallsService.cs +++ b/Core/Resgrid.Model/Services/ICallsService.cs @@ -419,7 +419,7 @@ Task ClearGroupForDispatchesAsync(int departmentGroupId, /// if set to true [get protocols]. /// if set to true [get references]. /// Task<Call>. - Task PopulateCallData(Call call, bool getDispatches, bool getAttachments, bool getNotes, bool getGroupDispatches, bool getUnitDispatches, bool getRoleDispatches, bool getProtocols, bool getReferences, bool getContacts); + Task PopulateCallData(Call call, bool getDispatches, bool getAttachments, bool getNotes, bool getGroupDispatches, bool getUnitDispatches, bool getRoleDispatches, bool getProtocols, bool getReferences, bool getContacts, bool getVideoFeeds = false); Task> GetAllNonDispatchedScheduledCallsWithinDateRange(DateTime startDate, DateTime endDate); @@ -433,5 +433,13 @@ Task ClearGroupForDispatchesAsync(int departmentGroupId, Task> GetCallsByContactIdAsync(string contactId, int departmentId); Task DeleteCallContactsAsync(int callId, CancellationToken cancellationToken = default(CancellationToken)); + + Task SaveCallVideoFeedAsync(CallVideoFeed feed, CancellationToken cancellationToken = default(CancellationToken)); + + Task GetCallVideoFeedByIdAsync(string callVideoFeedId); + + Task> GetCallVideoFeedsByCallIdAsync(int callId); + + Task DeleteCallVideoFeedAsync(CallVideoFeed feed, string deletedByUserId, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Core/Resgrid.Services/CallsService.cs b/Core/Resgrid.Services/CallsService.cs index 44649901..32d2e92b 100644 --- a/Core/Resgrid.Services/CallsService.cs +++ b/Core/Resgrid.Services/CallsService.cs @@ -42,6 +42,7 @@ public class CallsService : ICallsService private readonly ICallReferencesRepository _callReferencesRepository; private readonly ICallContactsRepository _callContactsRepository; private readonly IIndoorMapService _indoorMapService; + private readonly ICallVideoFeedRepository _callVideoFeedRepository; public CallsService(ICallsRepository callsRepository, ICommunicationService communicationService, ICallDispatchesRepository callDispatchesRepository, ICallTypesRepository callTypesRepository, ICallEmailFactory callEmailFactory, @@ -51,7 +52,7 @@ public CallsService(ICallsRepository callsRepository, ICommunicationService comm IDepartmentCallPriorityRepository departmentCallPriorityRepository, IShortenUrlProvider shortenUrlProvider, ICallProtocolsRepository callProtocolsRepository, IGeoLocationProvider geoLocationProvider, IDepartmentsService departmentsService, ICallReferencesRepository callReferencesRepository, ICallContactsRepository callContactsRepository, - IIndoorMapService indoorMapService) + IIndoorMapService indoorMapService, ICallVideoFeedRepository callVideoFeedRepository) { _callsRepository = callsRepository; _communicationService = communicationService; @@ -72,6 +73,7 @@ public CallsService(ICallsRepository callsRepository, ICommunicationService comm _callReferencesRepository = callReferencesRepository; _callContactsRepository = callContactsRepository; _indoorMapService = indoorMapService; + _callVideoFeedRepository = callVideoFeedRepository; } public async Task SaveCallAsync(Call call, CancellationToken cancellationToken = default(CancellationToken)) @@ -498,7 +500,7 @@ public async Task> GetActiveCallPrioritiesForDepart return activePriorities; } - public async Task PopulateCallData(Call call, bool getDispatches, bool getAttachments, bool getNotes, bool getGroupDispatches, bool getUnitDispatches, bool getRoleDispatches, bool getProtocols, bool getReferences, bool getContacts) + public async Task PopulateCallData(Call call, bool getDispatches, bool getAttachments, bool getNotes, bool getGroupDispatches, bool getUnitDispatches, bool getRoleDispatches, bool getProtocols, bool getReferences, bool getContacts, bool getVideoFeeds = false) { if (call == null) return null; @@ -585,6 +587,15 @@ public async Task PopulateCallData(Call call, bool getDispatches, bool get else call.Contacts = new List(); } + if (getVideoFeeds && call.VideoFeeds == null) + { + var items = await _callVideoFeedRepository.GetByCallIdAsync(call.CallId); + + if (items != null) + call.VideoFeeds = items.OrderBy(f => f.SortOrder).ToList(); + else + call.VideoFeeds = new List(); + } return call; } @@ -987,5 +998,38 @@ public async Task> GetCallsByContactIdAsync(string contactId, int dep return new List(); } + + public async Task SaveCallVideoFeedAsync(CallVideoFeed feed, CancellationToken cancellationToken = default(CancellationToken)) + { + var saved = await _callVideoFeedRepository.SaveOrUpdateAsync(feed, cancellationToken); + return saved; + } + + public async Task GetCallVideoFeedByIdAsync(string callVideoFeedId) + { + var feed = await _callVideoFeedRepository.GetByIdAsync(callVideoFeedId); + return feed; + } + + public async Task> GetCallVideoFeedsByCallIdAsync(int callId) + { + var feeds = await _callVideoFeedRepository.GetByCallIdAsync(callId); + + if (feeds != null && feeds.Any()) + return feeds.OrderBy(f => f.SortOrder).ToList(); + + return new List(); + } + + public async Task DeleteCallVideoFeedAsync(CallVideoFeed feed, string deletedByUserId, CancellationToken cancellationToken = default(CancellationToken)) + { + feed.IsDeleted = true; + feed.DeletedByUserId = deletedByUserId; + feed.DeletedOn = DateTime.UtcNow; + + await _callVideoFeedRepository.SaveOrUpdateAsync(feed, cancellationToken); + + return true; + } } } diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0061_AddingCallVideoFeeds.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0061_AddingCallVideoFeeds.cs new file mode 100644 index 00000000..eb71e9eb --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0061_AddingCallVideoFeeds.cs @@ -0,0 +1,58 @@ +using FluentMigrator; + +namespace Resgrid.Providers.Migrations.Migrations +{ + [Migration(61)] + public class M0061_AddingCallVideoFeeds : Migration + { + public override void Up() + { + Create.Table("CallVideoFeeds") + .WithColumn("CallVideoFeedId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("CallId").AsInt32().NotNullable() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("Name").AsString(500).NotNullable() + .WithColumn("Url").AsString(2000).NotNullable() + .WithColumn("FeedType").AsInt32().Nullable() + .WithColumn("FeedFormat").AsInt32().Nullable() + .WithColumn("Description").AsString(4000).Nullable() + .WithColumn("Status").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("Latitude").AsDecimal(10, 7).Nullable() + .WithColumn("Longitude").AsDecimal(10, 7).Nullable() + .WithColumn("AddedByUserId").AsString(128).NotNullable() + .WithColumn("AddedOn").AsDateTime2().NotNullable() + .WithColumn("UpdatedOn").AsDateTime2().Nullable() + .WithColumn("SortOrder").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("IsDeleted").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("DeletedByUserId").AsString(128).Nullable() + .WithColumn("DeletedOn").AsDateTime2().Nullable() + .WithColumn("IsFlagged").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("FlaggedReason").AsString(4000).Nullable() + .WithColumn("FlaggedByUserId").AsString(128).Nullable() + .WithColumn("FlaggedOn").AsDateTime2().Nullable(); + + Create.ForeignKey("FK_CallVideoFeeds_Calls") + .FromTable("CallVideoFeeds").ForeignColumn("CallId") + .ToTable("Calls").PrimaryColumn("CallId"); + + Create.ForeignKey("FK_CallVideoFeeds_Departments") + .FromTable("CallVideoFeeds").ForeignColumn("DepartmentId") + .ToTable("Departments").PrimaryColumn("DepartmentId"); + + Create.Index("IX_CallVideoFeeds_CallId") + .OnTable("CallVideoFeeds") + .OnColumn("CallId"); + + Create.Index("IX_CallVideoFeeds_DepartmentId") + .OnTable("CallVideoFeeds") + .OnColumn("DepartmentId"); + } + + public override void Down() + { + Delete.ForeignKey("FK_CallVideoFeeds_Calls").OnTable("CallVideoFeeds"); + Delete.ForeignKey("FK_CallVideoFeeds_Departments").OnTable("CallVideoFeeds"); + Delete.Table("CallVideoFeeds"); + } + } +} diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0061_AddingCallVideoFeedsPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0061_AddingCallVideoFeedsPg.cs new file mode 100644 index 00000000..4ac08fa2 --- /dev/null +++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0061_AddingCallVideoFeedsPg.cs @@ -0,0 +1,58 @@ +using FluentMigrator; + +namespace Resgrid.Providers.MigrationsPg.Migrations +{ + [Migration(61)] + public class M0061_AddingCallVideoFeedsPg : Migration + { + public override void Up() + { + Create.Table("callvideofeeds") + .WithColumn("callvideofeedid").AsCustom("citext").NotNullable().PrimaryKey() + .WithColumn("callid").AsInt32().NotNullable() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("name").AsCustom("citext").NotNullable() + .WithColumn("url").AsCustom("text").NotNullable() + .WithColumn("feedtype").AsInt32().Nullable() + .WithColumn("feedformat").AsInt32().Nullable() + .WithColumn("description").AsCustom("text").Nullable() + .WithColumn("status").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("latitude").AsDecimal(10, 7).Nullable() + .WithColumn("longitude").AsDecimal(10, 7).Nullable() + .WithColumn("addedbyuserid").AsCustom("citext").NotNullable() + .WithColumn("addedon").AsDateTime().NotNullable() + .WithColumn("updatedon").AsDateTime().Nullable() + .WithColumn("sortorder").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("isdeleted").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("deletedbyuserid").AsCustom("citext").Nullable() + .WithColumn("deletedon").AsDateTime().Nullable() + .WithColumn("isflagged").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("flaggedreason").AsCustom("text").Nullable() + .WithColumn("flaggedbyuserid").AsCustom("citext").Nullable() + .WithColumn("flaggedon").AsDateTime().Nullable(); + + Create.ForeignKey("fk_callvideofeeds_calls") + .FromTable("callvideofeeds").ForeignColumn("callid") + .ToTable("calls").PrimaryColumn("callid"); + + Create.ForeignKey("fk_callvideofeeds_departments") + .FromTable("callvideofeeds").ForeignColumn("departmentid") + .ToTable("departments").PrimaryColumn("departmentid"); + + Create.Index("ix_callvideofeeds_callid") + .OnTable("callvideofeeds") + .OnColumn("callid"); + + Create.Index("ix_callvideofeeds_departmentid") + .OnTable("callvideofeeds") + .OnColumn("departmentid"); + } + + public override void Down() + { + Delete.ForeignKey("fk_callvideofeeds_calls").OnTable("callvideofeeds"); + Delete.ForeignKey("fk_callvideofeeds_departments").OnTable("callvideofeeds"); + Delete.Table("callvideofeeds"); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/CallVideoFeedRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CallVideoFeedRepository.cs new file mode 100644 index 00000000..26b5032f --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/CallVideoFeedRepository.cs @@ -0,0 +1,108 @@ +using Dapper; +using Resgrid.Framework; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Queries.CallVideoFeeds; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; + +namespace Resgrid.Repositories.DataRepository +{ + public class CallVideoFeedRepository : RepositoryBase, ICallVideoFeedRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public CallVideoFeedRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetByCallIdAsync(int callId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("CallId", callId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + + public async Task> GetByDepartmentIdAsync(int departmentId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParametersExtension(); + dynamicParameters.Add("DepartmentId", departmentId); + + var query = _queryFactory.GetQuery(); + + return await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction); + }); + + DbConnection conn = null; + if (_unitOfWork?.Connection == null) + { + using (conn = _connectionProvider.Create()) + { + await conn.OpenAsync(); + return await selectFunction(conn); + } + } + else + { + conn = _unitOfWork.CreateOrGetConnection(); + return await selectFunction(conn); + } + } + catch (Exception ex) + { + Logging.LogException(ex); + throw; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs index e086d2ee..fa4c9572 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs @@ -330,6 +330,9 @@ protected SqlConfiguration() { } public string SelectFlaggedCallNotesByDepartmentIdQuery { get; set; } public string SelectFlaggedCallImagesByDepartmentIdQuery { get; set; } public string SelectFlaggedCallFilesByDepartmentIdQuery { get; set; } + public string CallVideoFeedsTable { get; set; } + public string SelectCallVideoFeedsByCallIdQuery { get; set; } + public string SelectCallVideoFeedsByDepartmentIdQuery { get; set; } #endregion Calls #region Dispatch Protocols diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs index 220ee03f..b9258431 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs @@ -133,6 +133,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs index 48a8e786..2ad5bf73 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs @@ -132,6 +132,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs index d92d974c..7b0ce678 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs @@ -132,6 +132,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs index 90b38dbb..5d56d8fc 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs @@ -132,6 +132,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByCallIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByCallIdQuery.cs new file mode 100644 index 00000000..85707dfb --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByCallIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.CallVideoFeeds +{ + public class SelectCallVideoFeedsByCallIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectCallVideoFeedsByCallIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectCallVideoFeedsByCallIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.CallVideoFeedsTable, + _sqlConfiguration.ParameterNotation, + new string[] { "%CALLID%" }, + new string[] { "CallId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByDepartmentIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByDepartmentIdQuery.cs new file mode 100644 index 00000000..159a9336 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByDepartmentIdQuery.cs @@ -0,0 +1,33 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.CallVideoFeeds +{ + public class SelectCallVideoFeedsByDepartmentIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectCallVideoFeedsByDepartmentIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectCallVideoFeedsByDepartmentIdQuery + .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName, + _sqlConfiguration.CallVideoFeedsTable, + _sqlConfiguration.ParameterNotation, + new string[] { "%DEPARTMENTID%" }, + new string[] { "DepartmentId" }); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs index 924646e2..76d2e0b6 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs @@ -1114,6 +1114,9 @@ UPDATE CallDispatches SelectAllCallUnitDispsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE CallId = %CALLID%"; SelectAllCallRoleDispsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE CallId = %CALLID%"; SelectCallNotesByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE CallId = %CALLID%"; + CallVideoFeedsTable = "CallVideoFeeds"; + SelectCallVideoFeedsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE CallId = %CALLID% AND IsDeleted = false"; + SelectCallVideoFeedsByDepartmentIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE DepartmentId = %DEPARTMENTID% AND IsDeleted = false"; SelectCallYearsByDeptQuery = @" SELECT extract(year from c.LoggedOn) FROM Calls c WHERE c.DepartmentId = %DID% diff --git a/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs index 649c4878..8c7d8419 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs @@ -1078,6 +1078,9 @@ UPDATE CallDispatches SelectAllCallUnitDispsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [CallId] = %CALLID%"; SelectAllCallRoleDispsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [CallId] = %CALLID%"; SelectCallNotesByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [CallId] = %CALLID%"; + CallVideoFeedsTable = "CallVideoFeeds"; + SelectCallVideoFeedsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [CallId] = %CALLID% AND [IsDeleted] = 0"; + SelectCallVideoFeedsByDepartmentIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [DepartmentId] = %DEPARTMENTID% AND [IsDeleted] = 0"; SelectCallYearsByDeptQuery = @" SELECT DISTINCT YEAR(c.LoggedOn) FROM Calls c WHERE c.DepartmentId = %DID% diff --git a/Tests/Resgrid.Tests/Services/CallVideoFeedTests.cs b/Tests/Resgrid.Tests/Services/CallVideoFeedTests.cs new file mode 100644 index 00000000..2c96f226 --- /dev/null +++ b/Tests/Resgrid.Tests/Services/CallVideoFeedTests.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using Resgrid.Model; +using Resgrid.Model.Providers; +using Resgrid.Model.Repositories; +using Resgrid.Model.Services; +using Resgrid.Services; + +namespace Resgrid.Tests.Services +{ + [TestFixture] + public class CallVideoFeedTests + { + private Mock _callsRepo; + private Mock _communicationService; + private Mock _callDispatchesRepo; + private Mock _callTypesRepo; + private Mock _callEmailFactory; + private Mock _cacheProvider; + private Mock _callNotesRepo; + private Mock _callAttachmentRepo; + private Mock _callDispatchGroupRepo; + private Mock _callDispatchUnitRepo; + private Mock _callDispatchRoleRepo; + private Mock _callPriorityRepo; + private Mock _shortenUrlProvider; + private Mock _callProtocolsRepo; + private Mock _geoLocationProvider; + private Mock _departmentsService; + private Mock _callReferencesRepo; + private Mock _callContactsRepo; + private Mock _indoorMapService; + private Mock _callVideoFeedRepo; + private CallsService _service; + + [SetUp] + public void SetUp() + { + _callsRepo = new Mock(); + _communicationService = new Mock(); + _callDispatchesRepo = new Mock(); + _callTypesRepo = new Mock(); + _callEmailFactory = new Mock(); + _cacheProvider = new Mock(); + _callNotesRepo = new Mock(); + _callAttachmentRepo = new Mock(); + _callDispatchGroupRepo = new Mock(); + _callDispatchUnitRepo = new Mock(); + _callDispatchRoleRepo = new Mock(); + _callPriorityRepo = new Mock(); + _shortenUrlProvider = new Mock(); + _callProtocolsRepo = new Mock(); + _geoLocationProvider = new Mock(); + _departmentsService = new Mock(); + _callReferencesRepo = new Mock(); + _callContactsRepo = new Mock(); + _indoorMapService = new Mock(); + _callVideoFeedRepo = new Mock(); + + _service = new CallsService( + _callsRepo.Object, _communicationService.Object, _callDispatchesRepo.Object, + _callTypesRepo.Object, _callEmailFactory.Object, _cacheProvider.Object, + _callNotesRepo.Object, _callAttachmentRepo.Object, _callDispatchGroupRepo.Object, + _callDispatchUnitRepo.Object, _callDispatchRoleRepo.Object, _callPriorityRepo.Object, + _shortenUrlProvider.Object, _callProtocolsRepo.Object, _geoLocationProvider.Object, + _departmentsService.Object, _callReferencesRepo.Object, _callContactsRepo.Object, + _indoorMapService.Object, _callVideoFeedRepo.Object); + } + + [Test] + public async Task SaveCallVideoFeedAsync_ShouldSaveAndReturnFeed() + { + var feed = new CallVideoFeed + { + CallVideoFeedId = Guid.NewGuid().ToString(), + CallId = 1, + DepartmentId = 10, + Name = "Drone Feed", + Url = "rtsp://example.com/stream", + FeedType = (int)CallVideoFeedTypes.Drone, + FeedFormat = (int)CallVideoFeedFormats.RTSP, + AddedByUserId = "user1", + AddedOn = DateTime.UtcNow + }; + + _callVideoFeedRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(feed); + + var result = await _service.SaveCallVideoFeedAsync(feed); + + result.Should().NotBeNull(); + result.Name.Should().Be("Drone Feed"); + result.Url.Should().Be("rtsp://example.com/stream"); + _callVideoFeedRepo.Verify(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task GetCallVideoFeedsByCallIdAsync_ShouldReturnFeedsForCall() + { + var feeds = new List + { + new CallVideoFeed { CallVideoFeedId = "feed1", CallId = 1, Name = "Feed 1", Url = "http://example.com/1", IsDeleted = false }, + new CallVideoFeed { CallVideoFeedId = "feed2", CallId = 1, Name = "Feed 2", Url = "http://example.com/2", IsDeleted = false } + }; + + _callVideoFeedRepo.Setup(x => x.GetByCallIdAsync(1)).ReturnsAsync(feeds); + + var result = await _service.GetCallVideoFeedsByCallIdAsync(1); + + result.Should().HaveCount(2); + result[0].Name.Should().Be("Feed 1"); + result[1].Name.Should().Be("Feed 2"); + } + + [Test] + public async Task GetCallVideoFeedsByCallIdAsync_ShouldNotReturnDeletedFeeds() + { + var feeds = new List + { + new CallVideoFeed { CallVideoFeedId = "feed1", CallId = 1, Name = "Feed 1", Url = "http://example.com/1", IsDeleted = false }, + new CallVideoFeed { CallVideoFeedId = "feed2", CallId = 1, Name = "Feed 2", Url = "http://example.com/2", IsDeleted = true } + }; + + _callVideoFeedRepo.Setup(x => x.GetByCallIdAsync(1)).ReturnsAsync(feeds); + + var result = await _service.GetCallVideoFeedsByCallIdAsync(1); + + // The service returns all feeds from repo; filtering is done at the API layer + result.Should().HaveCount(2); + } + + [Test] + public async Task DeleteCallVideoFeedAsync_ShouldSoftDelete() + { + var feed = new CallVideoFeed + { + CallVideoFeedId = "feed1", + CallId = 1, + DepartmentId = 10, + Name = "Feed 1", + Url = "http://example.com/1", + IsDeleted = false + }; + + _callVideoFeedRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(feed); + + var result = await _service.DeleteCallVideoFeedAsync(feed, "deletingUser"); + + result.Should().BeTrue(); + feed.IsDeleted.Should().BeTrue(); + feed.DeletedByUserId.Should().Be("deletingUser"); + feed.DeletedOn.Should().NotBeNull(); + } + + [Test] + public async Task SaveCallVideoFeedAsync_WithCoordinates_ShouldPersistLocation() + { + var feed = new CallVideoFeed + { + CallVideoFeedId = Guid.NewGuid().ToString(), + CallId = 1, + DepartmentId = 10, + Name = "Traffic Cam", + Url = "http://example.com/traffic", + Latitude = 39.2771m, + Longitude = -119.772m, + AddedByUserId = "user1", + AddedOn = DateTime.UtcNow + }; + + _callVideoFeedRepo.Setup(x => x.SaveOrUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(feed); + + var result = await _service.SaveCallVideoFeedAsync(feed); + + result.Should().NotBeNull(); + result.Latitude.Should().Be(39.2771m); + result.Longitude.Should().Be(-119.772m); + } + + [Test] + public async Task GetCallVideoFeedByIdAsync_WithInvalidId_ShouldReturnNull() + { + _callVideoFeedRepo.Setup(x => x.GetByIdAsync("nonexistent")).ReturnsAsync((CallVideoFeed)null); + + var result = await _service.GetCallVideoFeedByIdAsync("nonexistent"); + + result.Should().BeNull(); + } + } +} diff --git a/Web/Resgrid.Web.Services/Controllers/v4/CallVideoFeedsController.cs b/Web/Resgrid.Web.Services/Controllers/v4/CallVideoFeedsController.cs new file mode 100644 index 00000000..b3c27ab3 --- /dev/null +++ b/Web/Resgrid.Web.Services/Controllers/v4/CallVideoFeedsController.cs @@ -0,0 +1,265 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Resgrid.Model.Services; +using Resgrid.Providers.Claims; +using System.Threading.Tasks; +using Resgrid.Web.Services.Helpers; +using System.Linq; +using Resgrid.Model; +using Resgrid.Web.Helpers; +using Resgrid.Web.Services.Models.v4.CallVideoFeeds; +using System; +using Resgrid.Model.Helpers; +using System.Net.Mime; +using System.Globalization; +using System.Threading; + +namespace Resgrid.Web.Services.Controllers.v4 +{ + /// + /// Video feeds attached to calls for live video monitoring during incidents + /// + [Route("api/v{VersionId:apiVersion}/[controller]")] + [ApiVersion("4.0")] + [ApiExplorerSettings(GroupName = "v4")] + public class CallVideoFeedsController : V4AuthenticatedApiControllerbase + { + #region Members and Constructors + private readonly ICallsService _callsService; + private readonly IDepartmentsService _departmentsService; + + public CallVideoFeedsController(ICallsService callsService, IDepartmentsService departmentsService) + { + _callsService = callsService; + _departmentsService = departmentsService; + } + #endregion Members and Constructors + + /// + /// Get video feeds for a call + /// + /// CallId of the call you want to get video feeds for + /// + [HttpGet("GetCallVideoFeeds")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task> GetCallVideoFeeds(string callId) + { + if (String.IsNullOrWhiteSpace(callId) || !int.TryParse(callId, out var cId)) + return BadRequest(); + + var result = new CallVideoFeedsResult(); + + var call = await _callsService.GetCallByIdAsync(cId); + var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); + + if (call == null) + { + ResponseHelper.PopulateV4ResponseNotFound(result); + return Ok(result); + } + + if (call.DepartmentId != DepartmentId) + return Unauthorized(); + + var feeds = await _callsService.GetCallVideoFeedsByCallIdAsync(cId); + + if (feeds != null && feeds.Any()) + { + foreach (var feed in feeds.Where(f => !f.IsDeleted)) + { + var fullName = await UserHelper.GetFullNameForUser(feed.AddedByUserId); + result.Data.Add(ConvertCallVideoFeed(feed, fullName, department)); + } + + result.PageSize = result.Data.Count; + result.Status = ResponseHelper.Success; + } + else + { + result.PageSize = 0; + result.Status = ResponseHelper.NotFound; + } + + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } + + /// + /// Saves a video feed to a call + /// + /// Video feed data + /// The cancellation token + /// ActionResult. + [HttpPost("SaveCallVideoFeed")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [Authorize(Policy = ResgridResources.Call_Create)] + public async Task> SaveCallVideoFeed(SaveCallVideoFeedInput input, CancellationToken cancellationToken) + { + if (!ModelState.IsValid) + return BadRequest(); + + if (!int.TryParse(input.CallId, out var parsedCallId)) + return BadRequest(); + + var call = await _callsService.GetCallByIdAsync(parsedCallId); + + if (call == null) + return BadRequest(); + + if (call.DepartmentId != DepartmentId) + return Unauthorized(); + + var result = new SaveCallVideoFeedResult(); + + var feed = new CallVideoFeed(); + feed.CallVideoFeedId = Guid.NewGuid().ToString(); + feed.CallId = parsedCallId; + feed.DepartmentId = DepartmentId; + feed.Name = input.Name; + feed.Url = input.Url; + feed.FeedType = input.FeedType; + feed.FeedFormat = input.FeedFormat; + feed.Description = input.Description; + feed.Status = (int)CallVideoFeedStatuses.Active; + feed.AddedByUserId = UserId; + feed.AddedOn = DateTime.UtcNow; + feed.SortOrder = input.SortOrder; + + if (!String.IsNullOrWhiteSpace(input.Latitude) && !String.IsNullOrWhiteSpace(input.Longitude)) + { + if (!decimal.TryParse(input.Latitude, NumberStyles.Number, CultureInfo.InvariantCulture, out var lat) || + !decimal.TryParse(input.Longitude, NumberStyles.Number, CultureInfo.InvariantCulture, out var lng)) + return BadRequest(); + + feed.Latitude = lat; + feed.Longitude = lng; + } + + var saved = await _callsService.SaveCallVideoFeedAsync(feed, cancellationToken); + + result.Id = saved.CallVideoFeedId; + result.PageSize = 0; + result.Status = ResponseHelper.Created; + ResponseHelper.PopulateV4ResponseData(result); + + return CreatedAtAction(nameof(GetCallVideoFeeds), new { callId = saved.CallId }, result); + } + + /// + /// Updates an existing video feed + /// + /// Video feed data with Id + /// The cancellation token + /// ActionResult. + [HttpPut("EditCallVideoFeed")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [Authorize(Policy = ResgridResources.Call_Update)] + public async Task> EditCallVideoFeed(EditCallVideoFeedInput input, CancellationToken cancellationToken) + { + if (!ModelState.IsValid) + return BadRequest(); + + var feed = await _callsService.GetCallVideoFeedByIdAsync(input.CallVideoFeedId); + + if (feed == null) + return BadRequest(); + + if (feed.DepartmentId != DepartmentId) + return Unauthorized(); + + var result = new SaveCallVideoFeedResult(); + + feed.Name = input.Name; + feed.Url = input.Url; + feed.FeedType = input.FeedType; + feed.FeedFormat = input.FeedFormat; + feed.Description = input.Description; + feed.Status = input.Status; + feed.SortOrder = input.SortOrder; + feed.UpdatedOn = DateTime.UtcNow; + + if (!String.IsNullOrWhiteSpace(input.Latitude) && !String.IsNullOrWhiteSpace(input.Longitude)) + { + if (!decimal.TryParse(input.Latitude, NumberStyles.Number, CultureInfo.InvariantCulture, out var lat) || + !decimal.TryParse(input.Longitude, NumberStyles.Number, CultureInfo.InvariantCulture, out var lng)) + return BadRequest(); + + feed.Latitude = lat; + feed.Longitude = lng; + } + + var saved = await _callsService.SaveCallVideoFeedAsync(feed, cancellationToken); + + result.Id = saved.CallVideoFeedId; + result.PageSize = 0; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } + + /// + /// Soft deletes a video feed + /// + /// The video feed Id to delete + /// The cancellation token + /// ActionResult. + [HttpDelete("DeleteCallVideoFeed")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [Authorize(Policy = ResgridResources.Call_Delete)] + public async Task> DeleteCallVideoFeed(string callVideoFeedId, CancellationToken cancellationToken) + { + if (String.IsNullOrWhiteSpace(callVideoFeedId)) + return BadRequest(); + + var feed = await _callsService.GetCallVideoFeedByIdAsync(callVideoFeedId); + + if (feed == null) + return BadRequest(); + + if (feed.DepartmentId != DepartmentId) + return Unauthorized(); + + var result = new DeleteCallVideoFeedResult(); + + await _callsService.DeleteCallVideoFeedAsync(feed, UserId, cancellationToken); + + result.PageSize = 0; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } + + public static CallVideoFeedResultData ConvertCallVideoFeed(CallVideoFeed feed, string fullName, Department department) + { + var feedResult = new CallVideoFeedResultData(); + feedResult.CallVideoFeedId = feed.CallVideoFeedId; + feedResult.CallId = feed.CallId.ToString(); + feedResult.Name = feed.Name; + feedResult.Url = feed.Url; + feedResult.FeedType = feed.FeedType; + feedResult.FeedFormat = feed.FeedFormat; + feedResult.Description = feed.Description; + feedResult.Status = feed.Status; + feedResult.Latitude = feed.Latitude; + feedResult.Longitude = feed.Longitude; + feedResult.AddedByUserId = feed.AddedByUserId; + feedResult.AddedOnFormatted = feed.AddedOn.TimeConverter(department).FormatForDepartment(department); + feedResult.AddedOnUtc = feed.AddedOn; + feedResult.SortOrder = feed.SortOrder; + feedResult.FullName = fullName; + + return feedResult; + } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/CallVideoFeedsResult.cs b/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/CallVideoFeedsResult.cs new file mode 100644 index 00000000..9b0d3346 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/CallVideoFeedsResult.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace Resgrid.Web.Services.Models.v4.CallVideoFeeds +{ + public class CallVideoFeedsResult : StandardApiResponseV4Base + { + public List Data { get; set; } + + public CallVideoFeedsResult() + { + Data = new List(); + } + } + + public class CallVideoFeedResultData + { + public string CallVideoFeedId { get; set; } + public string CallId { get; set; } + public string Name { get; set; } + public string Url { get; set; } + public int? FeedType { get; set; } + public int? FeedFormat { get; set; } + public string Description { get; set; } + public int Status { get; set; } + public decimal? Latitude { get; set; } + public decimal? Longitude { get; set; } + public string AddedByUserId { get; set; } + public string AddedOnFormatted { get; set; } + public DateTime AddedOnUtc { get; set; } + public int SortOrder { get; set; } + public string FullName { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/DeleteCallVideoFeedResult.cs b/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/DeleteCallVideoFeedResult.cs new file mode 100644 index 00000000..ee994531 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/DeleteCallVideoFeedResult.cs @@ -0,0 +1,6 @@ +namespace Resgrid.Web.Services.Models.v4.CallVideoFeeds +{ + public class DeleteCallVideoFeedResult : StandardApiResponseV4Base + { + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/EditCallVideoFeedInput.cs b/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/EditCallVideoFeedInput.cs new file mode 100644 index 00000000..cdd17890 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/EditCallVideoFeedInput.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; + +namespace Resgrid.Web.Services.Models.v4.CallVideoFeeds +{ + public class EditCallVideoFeedInput + { + [Required] + public string CallVideoFeedId { get; set; } + + [Required] + public string CallId { get; set; } + + [Required] + public string Name { get; set; } + + [Required] + public string Url { get; set; } + + [Range(0, int.MaxValue)] + public int? FeedType { get; set; } + + [Range(0, int.MaxValue)] + public int? FeedFormat { get; set; } + + public string Description { get; set; } + + [Range(0, int.MaxValue)] + public int Status { get; set; } + + public string Latitude { get; set; } + + public string Longitude { get; set; } + + [Range(0, int.MaxValue)] + public int SortOrder { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/SaveCallVideoFeedInput.cs b/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/SaveCallVideoFeedInput.cs new file mode 100644 index 00000000..250c4390 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/SaveCallVideoFeedInput.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; + +namespace Resgrid.Web.Services.Models.v4.CallVideoFeeds +{ + public class SaveCallVideoFeedInput + { + [Required] + public string CallId { get; set; } + + [Required] + public string Name { get; set; } + + [Required] + public string Url { get; set; } + + [Range(0, int.MaxValue)] + public int? FeedType { get; set; } + + [Range(0, int.MaxValue)] + public int? FeedFormat { get; set; } + + public string Description { get; set; } + + public string Latitude { get; set; } + + public string Longitude { get; set; } + + [Range(0, int.MaxValue)] + public int SortOrder { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/SaveCallVideoFeedResult.cs b/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/SaveCallVideoFeedResult.cs new file mode 100644 index 00000000..6b6142a2 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/CallVideoFeeds/SaveCallVideoFeedResult.cs @@ -0,0 +1,7 @@ +namespace Resgrid.Web.Services.Models.v4.CallVideoFeeds +{ + public class SaveCallVideoFeedResult : StandardApiResponseV4Base + { + public string Id { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml index d6bb449b..eee3c26d 100644 --- a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml +++ b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml @@ -328,6 +328,42 @@ + + + Video feeds attached to calls for live video monitoring during incidents + + + + + Get video feeds for a call + + CallId of the call you want to get video feeds for + + + + + Saves a video feed to a call + + Video feed data + The cancellation token + ActionResult. + + + + Updates an existing video feed + + Video feed data with Id + The cancellation token + ActionResult. + + + + Soft deletes a video feed + + The video feed Id to delete + The cancellation token + ActionResult. + Check-in timer operations for call accountability diff --git a/Web/Resgrid.Web.Services/Startup.cs b/Web/Resgrid.Web.Services/Startup.cs index c947c3f2..52ce92da 100644 --- a/Web/Resgrid.Web.Services/Startup.cs +++ b/Web/Resgrid.Web.Services/Startup.cs @@ -203,18 +203,6 @@ public void ConfigureServices(IServiceCollection services) {securityScheme, new string[] { }} }); - //options.SwaggerDoc("v3", - - // new OpenApiInfo - // { - // Title = "Resgrid API", - // Version = "v3", - // Description = "The Resgrid Computer Aided Dispatch (CAD) API reference. Documentation: https://resgrid-core.readthedocs.io/en/latest/api/index.html", - // Contact = new OpenApiContact() { Email = "team@resgrid.com", Name = "Resgrid Team", Url = new Uri("https://resgrid.com") }, - // TermsOfService = new Uri("https://resgrid.com/Public/Terms") - // } - //); - options.SwaggerDoc("v4", new OpenApiInfo diff --git a/Web/Resgrid.Web/Areas/User/Controllers/CalendarController.cs b/Web/Resgrid.Web/Areas/User/Controllers/CalendarController.cs index e8761ddb..07b0d391 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/CalendarController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/CalendarController.cs @@ -208,6 +208,12 @@ public async Task Edit(int id) ViewBag.Types = new SelectList(model.Types, "CalendarItemTypeId", "Name"); model.Item.Description = StringHelpers.StripHtmlTagsCharArray(model.Item.Description); + if (model.Item.RecurrenceType == 2 && model.Item.RepeatOnWeek > 0) + { + model.WeekdayOccurrence = model.Item.RepeatOnWeek; + model.WeekdayDayOfWeek = model.Item.RepeatOnDay; + } + return View(model); } @@ -249,6 +255,12 @@ public async Task Edit(EditCalendarEntry model, CancellationToken model.Item.CreatorUserId = UserId; model.Item.Entities = model.entities; + if (model.Item.RecurrenceType == 2) + { + model.Item.RepeatOnWeek = model.WeekdayOccurrence; + model.Item.RepeatOnDay = model.WeekdayDayOfWeek; + } + await _calendarService.UpdateCalendarItemAsync(model.Item, department.TimeZone, cancellationToken); // Add new attendees from entities and notify only the newly added ones diff --git a/Web/Resgrid.Web/Areas/User/Controllers/ContactsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/ContactsController.cs index 92002b7c..f6bd2855 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/ContactsController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/ContactsController.cs @@ -169,6 +169,9 @@ public async Task Add() ViewBag.TimeZones = new SelectList(TimeZones.Zones, "Key", "Value"); ViewBag.Languages = new SelectList(SupportedLocales.SupportedLanguagesMap, "Key", "Value"); + var categories = await _contactsService.GetContactCategoriesForDepartmentAsync(DepartmentId); + ViewBag.Categories = new SelectList(categories, "ContactCategoryId", "Name"); + var udfDefinition = await _userDefinedFieldsService.GetActiveDefinitionAsync(DepartmentId, (int)UdfEntityType.Contact); if (udfDefinition != null) { @@ -196,6 +199,9 @@ public async Task Add(AddContactView model, CancellationToken can ViewBag.Languages = new SelectList(SupportedLocales.SupportedLanguagesMap, "Key", "Value"); model.Department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); + var addPostCategories = await _contactsService.GetContactCategoriesForDepartmentAsync(DepartmentId); + ViewBag.Categories = new SelectList(addPostCategories, "ContactCategoryId", "Name"); + // They specified a street address for physical if (!String.IsNullOrWhiteSpace(model.PhysicalAddress1)) { @@ -420,6 +426,9 @@ public async Task Edit(string contactId) ViewBag.TimeZones = new SelectList(TimeZones.Zones, "Key", "Value"); ViewBag.Languages = new SelectList(SupportedLocales.SupportedLanguagesMap, "Key", "Value"); + var editCategories = await _contactsService.GetContactCategoriesForDepartmentAsync(DepartmentId); + ViewBag.Categories = new SelectList(editCategories, "ContactCategoryId", "Name", model.Contact.ContactCategoryId); + var udfDefinitionEdit = await _userDefinedFieldsService.GetActiveDefinitionAsync(DepartmentId, (int)UdfEntityType.Contact); if (udfDefinitionEdit != null) { @@ -450,6 +459,9 @@ public async Task Edit(EditContactView model, CancellationToken c ViewBag.Languages = new SelectList(SupportedLocales.SupportedLanguagesMap, "Key", "Value"); model.Department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); + var editPostCategories = await _contactsService.GetContactCategoriesForDepartmentAsync(DepartmentId); + ViewBag.Categories = new SelectList(editPostCategories, "ContactCategoryId", "Name"); + // They specified a street address for physical if (!String.IsNullOrWhiteSpace(model.PhysicalAddress1)) { diff --git a/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs b/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs index 5533209e..776c65f8 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs @@ -962,11 +962,15 @@ public async Task ViewCall(int callId) model.Stations = await _departmentGroupsService.GetAllStationGroupsForDepartmentAsync(DepartmentId); model.Protocols = await _protocolsService.GetAllProtocolsForDepartmentAsync(DepartmentId); model.ChildCalls = await _callsService.GetChildCallsForCallAsync(callId); - model.Call = await _callsService.PopulateCallData(model.Call, true, true, true, true, true, true, true, true, true); + model.Call = await _callsService.PopulateCallData(model.Call, true, true, true, true, true, true, true, true, true, true); if (model.Stations == null) model.Stations = new List(); + model.VideoFeeds = model.Call.VideoFeeds != null + ? model.Call.VideoFeeds.Where(f => !f.IsDeleted).ToList() + : new List(); + if (!String.IsNullOrEmpty(model.Call.GeoLocationData)) { string[] loc = model.Call.GeoLocationData.Split(char.Parse(",")); diff --git a/Web/Resgrid.Web/Areas/User/Models/Calendar/EditCalendarEntry.cs b/Web/Resgrid.Web/Areas/User/Models/Calendar/EditCalendarEntry.cs index fe5e11e9..54bac0f1 100644 --- a/Web/Resgrid.Web/Areas/User/Models/Calendar/EditCalendarEntry.cs +++ b/Web/Resgrid.Web/Areas/User/Models/Calendar/EditCalendarEntry.cs @@ -16,5 +16,7 @@ public class EditCalendarEntry public DateTime? RecurrenceEndLocal { get; set; } public bool IsRecurrenceParent { get; set; } public string entities { get; set; } + public int WeekdayOccurrence { get; set; } + public int WeekdayDayOfWeek { get; set; } } } diff --git a/Web/Resgrid.Web/Areas/User/Models/Calls/ViewCallView.cs b/Web/Resgrid.Web/Areas/User/Models/Calls/ViewCallView.cs index 446e4f65..fd020a83 100644 --- a/Web/Resgrid.Web/Areas/User/Models/Calls/ViewCallView.cs +++ b/Web/Resgrid.Web/Areas/User/Models/Calls/ViewCallView.cs @@ -27,6 +27,7 @@ public class ViewCallView: BaseUserModel public List Protocols { get; set; } public List ChildCalls { get; set; } public List Contacts { get; set; } + public List VideoFeeds { get; set; } = new List(); public string IsMapTabActive() { diff --git a/Web/Resgrid.Web/Areas/User/Views/Calendar/Edit.cshtml b/Web/Resgrid.Web/Areas/User/Views/Calendar/Edit.cshtml index 7d277497..510b9028 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Calendar/Edit.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Calendar/Edit.cshtml @@ -252,11 +252,11 @@
  • - +  @Html.TextBoxFor(m => m.Item.RepeatOnDay, new { onkeydown = "javascript:return false;" })
  • @@ -264,7 +264,8 @@ - @@ -273,7 +274,7 @@ - diff --git a/Web/Resgrid.Web/Areas/User/Views/Contacts/Add.cshtml b/Web/Resgrid.Web/Areas/User/Views/Contacts/Add.cshtml index 07ffcc8a..e06dc369 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Contacts/Add.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Contacts/Add.cshtml @@ -68,6 +68,14 @@  @localizer["Company"] +
    + +
    + +
    +
    diff --git a/Web/Resgrid.Web/Areas/User/Views/Contacts/Edit.cshtml b/Web/Resgrid.Web/Areas/User/Views/Contacts/Edit.cshtml index e40aa350..39c0ce6a 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Contacts/Edit.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Contacts/Edit.cshtml @@ -68,6 +68,14 @@  @localizer["Company"]
    +
    + +
    + +
    +
    diff --git a/Web/Resgrid.Web/Areas/User/Views/Dispatch/ViewCall.cshtml b/Web/Resgrid.Web/Areas/User/Views/Dispatch/ViewCall.cshtml index 2fae790d..51e65aa7 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Dispatch/ViewCall.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Dispatch/ViewCall.cshtml @@ -181,6 +181,7 @@
  • +
  • @if (!String.IsNullOrWhiteSpace(Model.Call.CallFormData)) {
  • @@ -709,6 +710,68 @@
    +
    + @if (Model.VideoFeeds != null && Model.VideoFeeds.Any()) + { + + + + + + + + + + + + + + @foreach (var feed in Model.VideoFeeds.OrderBy(f => f.SortOrder)) + { + + + + + + + + + + } + +
    @localizer["VideoFeedName"]@localizer["VideoFeedUrl"]@localizer["VideoFeedType"]@localizer["VideoFeedFormat"]@localizer["VideoFeedStatus"]@localizer["VideoFeedAddedBy"]@localizer["VideoFeedAddedOn"]
    @feed.Name@feed.Url + @if (feed.FeedType.HasValue) + { + @(((Resgrid.Model.CallVideoFeedTypes)feed.FeedType.Value).ToString()) + } + + @if (feed.FeedFormat.HasValue) + { + @(((Resgrid.Model.CallVideoFeedFormats)feed.FeedFormat.Value).ToString()) + } + + @{ + var status = (Resgrid.Model.CallVideoFeedStatuses)feed.Status; + } + @if (status == Resgrid.Model.CallVideoFeedStatuses.Active) + { + @localizer["VideoFeedActive"] + } + else if (status == Resgrid.Model.CallVideoFeedStatuses.Inactive) + { + @localizer["VideoFeedInactive"] + } + else + { + @localizer["VideoFeedError"] + } + @feed.AddedByUserId@feed.AddedOn.TimeConverterToString(Model.Department)
    + } + else + { +

    @localizer["NoVideoFeeds"]

    + } +
    @if (!String.IsNullOrWhiteSpace(Model.Call.CallFormData)) {
    diff --git a/Web/Resgrid.Web/Areas/User/Views/Groups/Geofence.cshtml b/Web/Resgrid.Web/Areas/User/Views/Groups/Geofence.cshtml index 464d1182..f6cbc733 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Groups/Geofence.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Groups/Geofence.cshtml @@ -43,6 +43,9 @@ @Html.HiddenFor(m => m.Group.DepartmentGroupId)
    + + +
    @Model.Group.Name
    diff --git a/Web/Resgrid.Web/wwwroot/js/app/internal/calendar/resgrid.calendar.editEntry.js b/Web/Resgrid.Web/wwwroot/js/app/internal/calendar/resgrid.calendar.editEntry.js index 359abdb8..5bff419f 100644 --- a/Web/Resgrid.Web/wwwroot/js/app/internal/calendar/resgrid.calendar.editEntry.js +++ b/Web/Resgrid.Web/wwwroot/js/app/internal/calendar/resgrid.calendar.editEntry.js @@ -70,6 +70,24 @@ var resgrid; $("#Item_RepeatOnDay").attr({ type: 'number', min: 1, max: 31, step: 1 }); + // Toggle between "day of month" and "weekday occurrence" inputs + $('input[name="month"]').on('change', function () { + if ($(this).val() === 'weekday') { + $('#Item_RepeatOnDay').prop('disabled', true).removeAttr('min max'); + $('#WeekdayOccurrence, #WeekdayDayOfWeek').prop('disabled', false); + } else { + $('#Item_RepeatOnDay').prop('disabled', false).attr({ min: 1, max: 31 }); + $('#WeekdayOccurrence, #WeekdayDayOfWeek').prop('disabled', true); + } + }); + + // Determine initial radio state from existing data + if ($('#Item_RepeatOnWeek').length && parseInt($('#Item_RepeatOnWeek').val()) > 0) { + $('input[name="month"][value="weekday"]').prop('checked', true).trigger('change'); + } else { + $('input[name="month"][value="monthday"]').prop('checked', true).trigger('change'); + } + let quill = new Quill('#editor-container', { placeholder: '', theme: 'snow' diff --git a/Web/Resgrid.Web/wwwroot/js/app/internal/calendar/resgrid.calendar.newEntry.js b/Web/Resgrid.Web/wwwroot/js/app/internal/calendar/resgrid.calendar.newEntry.js index 8ad1058f..5eea4596 100644 --- a/Web/Resgrid.Web/wwwroot/js/app/internal/calendar/resgrid.calendar.newEntry.js +++ b/Web/Resgrid.Web/wwwroot/js/app/internal/calendar/resgrid.calendar.newEntry.js @@ -66,6 +66,20 @@ var resgrid; $("#Item_RepeatOnDay").attr({ type: 'number', min: 1, max: 31, step: 1 }); + // Toggle between "day of month" and "weekday occurrence" inputs + $('input[name="month"]').on('change', function () { + if ($(this).val() === 'weekday') { + $('#Item_RepeatOnDay').prop('disabled', true).removeAttr('min max'); + $('#WeekdayOccurrence, #WeekdayDayOfWeek').prop('disabled', false); + } else { + $('#Item_RepeatOnDay').prop('disabled', false).attr({ min: 1, max: 31 }); + $('#WeekdayOccurrence, #WeekdayDayOfWeek').prop('disabled', true); + } + }); + + // Default to "monthday" radio selected, weekday dropdowns disabled + $('input[name="month"][value="monthday"]').prop('checked', true).trigger('change'); + var quill = new Quill('#editor-container', { placeholder: '', theme: 'snow' diff --git a/Web/Resgrid.Web/wwwroot/js/app/internal/groups/resgrid.groups.geofence.js b/Web/Resgrid.Web/wwwroot/js/app/internal/groups/resgrid.groups.geofence.js index 2c861a61..8a137fa1 100644 --- a/Web/Resgrid.Web/wwwroot/js/app/internal/groups/resgrid.groups.geofence.js +++ b/Web/Resgrid.Web/wwwroot/js/app/internal/groups/resgrid.groups.geofence.js @@ -94,6 +94,14 @@ var resgrid; }); } + var groupId = $('#Group_DepartmentGroupId').val(); + if (!groupId) { + $('#alertArea').html('

    Unable to determine the group. Please reload the page and try again.

    '); + $('#successArea').hide(); + $('#alertArea').show(); + return; + } + $.ajax({ type: "POST", async: true, @@ -102,14 +110,20 @@ var resgrid; cache: false, processData: false, data: JSON.stringify({ - DepartmentGroupId: $('#Group_DepartmentGroupId').val(), + DepartmentGroupId: parseInt(groupId, 10), Color: $('#colorPicker').val(), GeoFence: JSON.stringify(coords) }) }).done(function (data) { - $('#successArea').html('

    Your geofence has been saved.

    '); - $('#successArea').show(); - $('#alertArea').hide(); + if (data && data.Success) { + $('#successArea').html('

    ' + (data.Message || 'Your geofence has been saved.') + '

    '); + $('#successArea').show(); + $('#alertArea').hide(); + } else { + $('#alertArea').html('

    ' + (data && data.Message ? data.Message : 'Your geofence could not be saved.') + '

    '); + $('#successArea').hide(); + $('#alertArea').show(); + } }).fail(function (error) { $('#alertArea').html('

    Your geofence could not be saved. Please correct the errors and try again.

    '); $('#successArea').hide();