From fd66eca76428df175f9d043abd7a9431cee2f312 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Sat, 26 Aug 2023 19:32:29 -0700 Subject: [PATCH 01/26] #86696cxr4 Sync from upstream First cut of multi-lingual for a lot of pages, starting to remove kendo from suite (tables first), lots of bug fixes, some async bug fixes, new system queue, department audio, new personnel and units pages, livekit support (preferred voip). --- .github/workflows/auto-approve.yml | 26 + Core/Resgrid.Config/ExternalErrorConfig.cs | 10 +- Core/Resgrid.Config/MappingConfig.cs | 5 + Core/Resgrid.Config/VoipConfig.cs | 8 +- Core/Resgrid.Framework/DateTimeHelpers.cs | 31 +- Core/Resgrid.Framework/LocationHelpers.cs | 4 + Core/Resgrid.Framework/Logging.cs | 3 +- Core/Resgrid.Framework/StringHelpers.cs | 12 + Core/Resgrid.Localization/Account/Login.cs | 10 + .../Account/Login.en.resx | 255 + .../Account/Login.es.resx | 255 + .../Areas/User/Account/DeleteAccount.cs | 10 + .../Areas/User/Account/DeleteAccount.en.resx | 141 + .../Areas/User/Calendar/Calendar.cs | 10 + .../Areas/User/Calendar/Calendar.en.resx | 249 + .../Areas/User/Calendar/Calendar.es.resx | 249 + .../User/CustomStatuses/CustomStatuses.cs | 10 + .../CustomStatuses/CustomStatuses.en.resx | 246 + .../CustomStatuses/CustomStatuses.es.resx | 246 + .../Areas/User/Department/Department.cs | 10 + .../Areas/User/Department/Department.en.resx | 396 + .../Areas/User/Department/Department.es.resx | 381 + .../Areas/User/Department/Department.resx | 23 + .../Areas/User/Dispatch/Call.cs | 10 + .../Areas/User/Dispatch/Call.en.resx | 471 + .../Areas/User/Dispatch/Call.es.resx | 471 + .../Areas/User/Dispatch/Dashboard.cs | 10 + .../Areas/User/Dispatch/Dashboard.en.resx | 135 + .../Areas/User/Dispatch/Dashboard.es.resx | 135 + .../Areas/User/Home/EditProfile.cs | 10 + .../Areas/User/Home/EditProfile.en.resx | 378 + .../Areas/User/Home/EditProfile.es.resx | 372 + .../Areas/User/Home/HomeDashboard.cs | 10 + .../Areas/User/Home/HomeDashboard.en.resx | 174 + .../Areas/User/Home/HomeDashboard.es.resx | 174 + .../Areas/User/Logs/Logs.cs | 10 + .../Areas/User/Logs/Logs.en.resx | 261 + .../Areas/User/Logs/Logs.es.resx | 261 + .../Areas/User/Messages/Messages.cs | 10 + .../Areas/User/Messages/Messages.en.resx | 222 + .../Areas/User/Messages/Messages.es.resx | 222 + .../Areas/User/Notes/Note.cs | 10 + .../Areas/User/Notes/Note.en.resx | 165 + .../Areas/User/Notes/Note.es.resx | 165 + .../Areas/User/Personnel/Person.cs | 10 + .../Areas/User/Personnel/Person.en.resx | 369 + .../Areas/User/Personnel/Person.es.resx | 369 + .../Areas/User/Profile/Profile.cs | 10 + .../Areas/User/Profile/Profile.en.resx | 291 + .../Areas/User/Profile/Profile.es.resx | 291 + .../Areas/User/Subscription/Subscription.cs | 7 + .../User/Subscription/Subscription.en.resx | 471 + .../User/Subscription/Subscription.es.resx | 468 + .../Areas/User/Subscription/Subscription.resx | 23 + .../Areas/User/Units/Units.cs | 10 + .../Areas/User/Units/Units.en.resx | 225 + .../Areas/User/Units/Units.es.resx | 225 + .../Areas/User/Voice/Voice.cs | 10 + .../Areas/User/Voice/Voice.en.resx | 165 + .../Areas/User/Voice/Voice.es.resx | 101 + Core/Resgrid.Localization/Common.cs | 10 + Core/Resgrid.Localization/Common.en.resx | 651 + Core/Resgrid.Localization/Common.es.resx | 648 + .../Resgrid.Localization.csproj | 21 + Core/Resgrid.Localization/SupportedLocales.cs | 35 + Core/Resgrid.Model/AuditLogTypes.cs | 6 +- Core/Resgrid.Model/Call.cs | 22 +- Core/Resgrid.Model/CallReference.cs | 68 + Core/Resgrid.Model/DeleteGroupResults.cs | 9 +- Core/Resgrid.Model/DepartmentAudio.cs | 49 + Core/Resgrid.Model/DepartmentSettingTypes.cs | 3 +- .../DepartmentSuppressStaffingInfo.cs | 20 + .../DepartmentVoiceUtilization.cs | 12 + .../Events/NewChatNotificationEvent.cs | 3 + .../IStripeSubscriptionServiceFacade.cs | 5 +- Core/Resgrid.Model/Payment.cs | 8 + Core/Resgrid.Model/PaymentAddon.cs | 3 + Core/Resgrid.Model/PlanAddon.cs | 14 +- .../Resgrid.Model/Providers/IEmailProvider.cs | 7 +- Core/Resgrid.Model/QueueItem.cs | 19 +- Core/Resgrid.Model/QueueTypes.cs | 10 +- .../Repositories/ICallReferencesRepository.cs | 16 + .../Repositories/IDeleteRepository.cs | 19 + .../IDepartmentAudioRepository.cs | 13 + .../Repositories/IIdentityRepository.cs | 4 + .../Resgrid.Model/Services/IAddressService.cs | 1 + .../Services/IAuthorizationService.cs | 2 + Core/Resgrid.Model/Services/ICallsService.cs | 9 +- .../Services/ICommunicationService.cs | 3 +- Core/Resgrid.Model/Services/IDeleteService.cs | 7 + .../Services/IDepartmentGroupsService.cs | 2 + .../Services/IDepartmentSettingsService.cs | 2 + Core/Resgrid.Model/Services/IEmailService.cs | 2 + .../Services/IPaymentProviderService.cs | 27 +- Core/Resgrid.Model/Services/IQueueService.cs | 10 + Core/Resgrid.Model/Services/ISmsService.cs | 4 +- .../Services/ISubscriptionsService.cs | 4 + Core/Resgrid.Model/Services/IUsersService.cs | 1 + Core/Resgrid.Model/Services/IVoiceService.cs | 11 + Core/Resgrid.Model/TimeZones.cs | 22 +- Core/Resgrid.Model/UserProfile.cs | 3 + Core/Resgrid.Services/AddressService.cs | 37 +- Core/Resgrid.Services/AuthorizationService.cs | 19 +- Core/Resgrid.Services/CallsService.cs | 69 +- Core/Resgrid.Services/CommunicationService.cs | 69 +- Core/Resgrid.Services/CustomStateService.cs | 38 +- Core/Resgrid.Services/DeleteService.cs | 189 +- .../DepartmentGroupsService.cs | 11 +- .../DepartmentSettingsService.cs | 122 +- Core/Resgrid.Services/DepartmentsService.cs | 1 + Core/Resgrid.Services/EmailService.cs | 37 +- .../Stripe/StripeSubscriptionServiceFacade.cs | 16 + .../PaymentProviderService.cs | 45 + Core/Resgrid.Services/QueueService.cs | 57 +- Core/Resgrid.Services/Resgrid.Services.csproj | 1 + .../Resgrid.Services/ScheduledTasksService.cs | 6 +- Core/Resgrid.Services/ShiftsService.cs | 18 +- Core/Resgrid.Services/SmsService.cs | 31 +- Core/Resgrid.Services/SubscriptionsService.cs | 10 + Core/Resgrid.Services/UnitsService.cs | 55 +- Core/Resgrid.Services/UsersService.cs | 49 +- Core/Resgrid.Services/VoiceService.cs | 66 +- .../LoqateProvider.cs | 6 + .../RabbitInboundEventProvider.cs | 44 +- .../RabbitInboundQueueProvider.cs | 12 +- .../RabbitTopicProvider.cs | 27 +- .../Resgrid.Providers.Bus/SignalrProvider.cs | 10 +- .../Resgrid.Providers.Cache.csproj | 2 +- .../Resgrid.Providers.Email/EmailSender.cs | 72 +- .../PostmarkTemplateProvider.cs | 270 +- .../Resgrid.Providers.Email.csproj | 1 + .../Template/DeleteDepartment.html | 475 + .../MailerliteEmailMarketing.cs | 70 +- .../M0020_AddingLangToUPAndSystemQ.cs | 26 + .../Migrations/M0021_AddingCallReferences.cs | 33 + .../M0022_AddingQuantityForPlanAddon.cs | 19 + .../M0023_AddingPlanAddonFor10Pack.cs | 26 + .../Migrations/M0024_AddingDepartmentAudio.cs | 30 + .../M0025_AddingTypeToDepartmentAudio.cs | 19 + .../TextMessageProvider.cs | 52 +- .../LiveKit/LiveKitAccessToken.cs | 81 + .../LiveKit/LiveKitGrant.cs | 22 + .../LiveKit/LiveKitVideoGrant.cs | 23 + .../Model/LiveKitAccessTokenOptions.cs | 31 + .../LiveKit/Model/LiveKitParticipantInfo.cs | 37 + .../LiveKit/Model/LiveKitParticipants.cs | 14 + .../Resgrid.Providers.Voip/LiveKitProvider.cs | 63 + .../Resgrid.Providers.Voip.csproj | 6 + .../CallReferencesRepository.cs | 123 + .../Configs/SqlConfiguration.cs | 30 +- .../DeleteRepository.cs | 179 + .../DepartmentAudioRepository.cs | 34 + .../IdentityRepository.cs | 26 + .../Modules/ApiDataModule.cs | 3 + .../Modules/DataModule.cs | 3 + .../Modules/NonWebDataModule.cs | 3 + .../Modules/TestingDataModule.cs | 3 + ...ectAllCallReferencesBySourceCallIdQuery.cs | 46 + ...ectAllCallReferencesByTargetCallIdQuery.cs | 46 + .../SqlServer/SqlServerConfiguration.cs | 20 +- .../UnitsRepository.cs | 48 + ResgridCore.sln | 23 +- .../Services/CommunicationServiceTests.cs | 17 +- Web/Resgrid.Web.Eventing/Startup.cs | 2 +- .../Controllers/TwilioController.cs | 2 +- .../Controllers/v3/CallsController.cs | 16 +- .../Controllers/v3/ChatController.cs | 1 + .../Controllers/v4/CallFilesController.cs | 2 +- .../Controllers/v4/CallNotesController.cs | 2 +- .../Controllers/v4/CallsController.cs | 14 +- .../Controllers/v4/ConfigController.cs | 5 + .../Controllers/v4/PersonnelController.cs | 9 +- .../Controllers/v4/VoiceController.cs | 106 +- .../Middleware/ApiTelemetryInitializer.cs | 29 + .../Models/v4/Configs/GetConfigResult.cs | 5 + .../Voice/CanConnectToVoiceSessionResult.cs | 34 + .../Models/v4/Voice/DepartmentAudioResult.cs | 23 + .../Models/v4/Voice/DepartmentVoiceResult.cs | 4 + Web/Resgrid.Web.ServicesCore/Program.cs | 2 + .../Resgrid.Web.Services.xml | 52 + .../Resgrid.Web.ServicesCore.csproj | 2 + Web/Resgrid.Web.ServicesCore/Startup.cs | 18 +- .../componetns/omnibar/omnibar.component.css | 0 .../componetns/omnibar/omnibar.component.html | 0 .../componetns/omnibar/omnibar.component.ts | 0 .../User/Controllers/AccountController.cs | 33 +- .../User/Controllers/DepartmentController.cs | 240 +- .../User/Controllers/DispatchController.cs | 150 +- .../User/Controllers/DocumentsController.cs | 2 +- .../Areas/User/Controllers/HomeController.cs | 63 +- .../Areas/User/Controllers/LinksController.cs | 4 +- .../Areas/User/Controllers/NotesController.cs | 2 +- .../User/Controllers/PersonnelController.cs | 463 +- .../User/Controllers/ReportsController.cs | 235 +- .../User/Controllers/SearchController.cs | 182 + .../Areas/User/Controllers/UnitsController.cs | 218 +- .../Areas/User/Controllers/VoiceController.cs | 101 + .../User/Models/Account/DeleteAccountModel.cs | 4 +- .../Areas/User/Models/Calls/NewCallView.cs | 3 + .../Areas/User/Models/Calls/ViewCallView.cs | 1 + .../User/Models/DepartmentSettingsModel.cs | 4 + .../Departments/DeleteDepartmentView.cs | 14 + .../User/Models/Dispatch/CallExportView.cs | 1 + .../Models/Dispatch/CallSelectListJson.cs | 17 + .../Areas/User/Models/EditProfileModel.cs | 1 + .../User/Models/Personnel/PersonnelForJson.cs | 2 + .../Areas/User/Models/PersonnelModel.cs | 13 +- .../User/Models/Search/SearchResultJson.cs | 16 + .../User/Models/Voice/NewAudioStreamModel.cs | 17 + .../User/Models/Voice/VoiceIndexModel.cs | 2 + .../User/Views/Account/DeleteAccount.cshtml | 174 +- .../Areas/User/Views/Calendar/Edit.cshtml | 164 +- .../Areas/User/Views/Calendar/EditType.cshtml | 24 +- .../Areas/User/Views/Calendar/Index.cshtml | 207 +- .../Areas/User/Views/Calendar/New.cshtml | 148 +- .../Areas/User/Views/Calendar/NewType.cshtml | 26 +- .../Areas/User/Views/Calendar/Types.cshtml | 131 +- .../Areas/User/Views/Calendar/View.cshtml | 1033 +- .../User/Views/CustomStatuses/Edit.cshtml | 97 +- .../Views/CustomStatuses/EditDetail.cshtml | 33 +- .../User/Views/CustomStatuses/Index.cshtml | 44 +- .../User/Views/CustomStatuses/New.cshtml | 89 +- .../User/Views/Department/Address.cshtml | 30 +- .../Areas/User/Views/Department/Api.cshtml | 21 +- .../User/Views/Department/CallSettings.cshtml | 468 +- .../Views/Department/DeleteDepartment.cshtml | 90 + .../Views/Department/DispatchSettings.cshtml | 21 +- .../User/Views/Department/Invites.cshtml | 227 +- .../Views/Department/ProvisionFailed.cshtml | 19 +- .../User/Views/Department/Settings.cshtml | 563 +- .../Views/Dispatch/AddArchivedCall.cshtml | 416 +- .../User/Views/Dispatch/ArchivedCalls.cshtml | 21 +- .../Areas/User/Views/Dispatch/CallData.cshtml | 41 +- .../User/Views/Dispatch/CallExport.cshtml | 191 +- .../User/Views/Dispatch/CallExportEx.cshtml | 179 +- .../User/Views/Dispatch/CloseCall.cshtml | 24 +- .../User/Views/Dispatch/Dashboard.cshtml | 28 +- .../User/Views/Dispatch/DeleteCall.cshtml | 20 +- .../User/Views/Dispatch/FlagCallNote.cshtml | 67 +- .../Areas/User/Views/Dispatch/NewCall.cshtml | 166 +- .../User/Views/Dispatch/UpdateCall.cshtml | 141 +- .../Areas/User/Views/Dispatch/ViewCall.cshtml | 1359 +- .../_SmallActiveCallsGridPartial.cshtml | 2 +- .../Dispatch/_SmallCallsGridPartial.cshtml | 2 +- .../User/Views/Documents/EditDocument.cshtml | 1 + .../Areas/User/Views/Home/Dashboard.cshtml | 63 +- .../User/Views/Home/EditUserProfile.cshtml | 1040 +- .../_PersonnelActionButtonsPartial.cshtml | 26 +- .../Views/Home/_UserActionsPartial.cshtml | 18 +- .../Views/Home/_UserStatusTablePartial.cshtml | 27 +- .../Home/_UserStatusTableRowPartial.cshtml | 573 +- .../Areas/User/Views/Logs/Index.cshtml | 17 +- .../Areas/User/Views/Logs/NewLog.cshtml | 146 +- .../Views/Logs/_UnitLogBlockPartial.cshtml | 18 +- .../Areas/User/Views/Messages/Compose.cshtml | 36 +- .../Areas/User/Views/Messages/Inbox.cshtml | 19 +- .../Areas/User/Views/Messages/Outbox.cshtml | 73 +- .../User/Views/Messages/ViewMessage.cshtml | 91 +- .../Messages/_UnreadTopMessagesPartial.cshtml | 4 +- .../Areas/User/Views/Notes/Edit.cshtml | 161 +- .../Areas/User/Views/Notes/Index.cshtml | 85 +- .../Areas/User/Views/Notes/NewNote.cshtml | 154 +- .../Areas/User/Views/Notes/View.cshtml | 135 +- .../Views/Personnel/AddExistingUser.cshtml | 129 +- .../User/Views/Personnel/AddPerson.cshtml | 97 +- .../Areas/User/Views/Personnel/AddRole.cshtml | 27 +- .../User/Views/Personnel/DeletePerson.cshtml | 39 +- .../User/Views/Personnel/EditRole.cshtml | 32 +- .../Areas/User/Views/Personnel/Index.cshtml | 618 +- .../Areas/User/Views/Personnel/Ranks.cshtml | 169 +- .../Views/Personnel/ReactivateUser.cshtml | 163 +- .../Areas/User/Views/Personnel/Roles.cshtml | 157 +- .../User/Views/Personnel/ViewPerson.cshtml | 34 +- .../User/Views/Personnel/ViewRole.cshtml | 104 +- .../Views/Profile/AddCertification.cshtml | 46 +- .../Profile/AddNewScheduledReport.cshtml | 48 +- .../Profile/AddNewStaffingSchedule.cshtml | 48 +- .../User/Views/Profile/Certifications.cshtml | 240 +- .../Views/Profile/EditCertification.cshtml | 182 +- .../Views/Profile/EditScheduledReport.cshtml | 371 +- .../Views/Profile/EditStaffingSchedule.cshtml | 48 +- .../Areas/User/Views/Profile/Reporting.cshtml | 85 +- .../Views/Profile/ResetPasswordForUser.cshtml | 37 +- .../User/Views/Profile/ViewSchedules.cshtml | 29 +- .../User/Views/Profile/YourDepartments.cshtml | 39 +- .../User/Views/Shared/_Navigation.cshtml | 34 +- .../User/Views/Shared/_TopIconsPartial.cshtml | 4 +- .../Areas/User/Views/Shared/_TopNavbar.cshtml | 126 +- .../Views/Shared/_TopUpgradePartial.cshtml | 2 +- .../User/Views/Shared/_UserLayout.cshtml | 13 +- .../User/Views/Shifts/EditShiftDays.cshtml | 5 +- .../Areas/User/Views/Shifts/NewShift.cshtml | 23 +- .../Areas/User/Views/Trainings/Quiz.cshtml | 5 +- .../Areas/User/Views/Units/AddLog.cshtml | 130 +- .../Areas/User/Views/Units/EditUnit.cshtml | 187 +- .../Areas/User/Views/Units/Index.cshtml | 230 +- .../Areas/User/Views/Units/NewUnit.cshtml | 32 +- .../User/Views/Units/UnitStaffing.cshtml | 21 +- .../Areas/User/Views/Units/ViewEvents.cshtml | 25 +- .../Areas/User/Views/Units/ViewLogs.cshtml | 114 +- .../Views/Units/_SmallUnitsGridPartial.cshtml | 8 +- .../Areas/User/Views/Voice/EditAudio.cshtml | 79 + .../Areas/User/Views/Voice/Index.cshtml | 105 +- .../Areas/User/Views/Voice/NewAudio.cshtml | 78 + .../User/Views/Workshifts/ViewDay.cshtml | 79 +- .../Areas/User/Views/_ViewImports.cshtml | 7 + .../Controllers/AccountController.cs | 27 +- .../Helpers/CustomStatesHelper.cs | 12 +- .../Middleware/WebTelemetryInitializer.cs | 29 + Web/Resgrid.WebCore/Program.cs | 2 + .../PublishProfiles/FolderProfile.pubxml | 8 +- .../local/appInsights1.arm.json | 67 + .../Properties/serviceDependencies.json | 5 + .../Properties/serviceDependencies.local.json | 7 + Web/Resgrid.WebCore/Resgrid.WebCore.csproj | 3 + Web/Resgrid.WebCore/Startup.cs | 49 +- .../Views/Account/CompleteInvite.cshtml | 227 +- .../Views/Account/CompletedInvite.cshtml | 111 +- .../Views/Account/ForgotPassword.cshtml | 156 +- .../Account/ForgotPasswordConfirmation.cshtml | 92 +- .../Views/Account/Lockout.cshtml | 111 +- .../Views/Account/LogOn.cshtml | 242 +- .../Views/Account/MissingInvite.cshtml | 111 +- .../Views/Account/Register.cshtml | 42 +- Web/Resgrid.WebCore/Views/Shared/Error.cshtml | 38 +- .../Views/Shared/_Layout.cshtml | 215 +- .../Views/Shared/_ScriptsPartial.cshtml | 17 +- .../Views/Shared/_StylePartial.cshtml | 17 +- Web/Resgrid.WebCore/libman.json | 142 +- .../Buttons-2.4.1/css/buttons.bootstrap.css | 275 + .../css/buttons.bootstrap.min.css | 1 + .../Buttons-2.4.1/css/buttons.bootstrap4.css | 258 + .../css/buttons.bootstrap4.min.css | 1 + .../Buttons-2.4.1/css/buttons.bootstrap5.css | 258 + .../css/buttons.bootstrap5.min.css | 1 + .../Buttons-2.4.1/css/buttons.bulma.css | 453 + .../Buttons-2.4.1/css/buttons.bulma.min.css | 1 + .../Buttons-2.4.1/css/buttons.dataTables.css | 564 + .../css/buttons.dataTables.min.css | 1 + .../Buttons-2.4.1/css/buttons.foundation.css | 337 + .../css/buttons.foundation.min.css | 1 + .../Buttons-2.4.1/css/buttons.jqueryui.css | 405 + .../css/buttons.jqueryui.min.css | 1 + .../Buttons-2.4.1/css/buttons.semanticui.css | 467 + .../css/buttons.semanticui.min.css | 1 + .../data-tables/Buttons-2.4.1/css/common.scss | 159 + .../data-tables/Buttons-2.4.1/css/mixins.scss | 229 + .../Buttons-2.4.1/js/buttons.bootstrap.js | 118 + .../Buttons-2.4.1/js/buttons.bootstrap.min.js | 4 + .../Buttons-2.4.1/js/buttons.bootstrap4.js | 117 + .../js/buttons.bootstrap4.min.js | 4 + .../Buttons-2.4.1/js/buttons.bootstrap5.js | 117 + .../js/buttons.bootstrap5.min.js | 4 + .../Buttons-2.4.1/js/buttons.bulma.js | 131 + .../Buttons-2.4.1/js/buttons.bulma.min.js | 4 + .../Buttons-2.4.1/js/buttons.colVis.js | 260 + .../Buttons-2.4.1/js/buttons.colVis.min.js | 5 + .../Buttons-2.4.1/js/buttons.dataTables.js | 58 + .../js/buttons.dataTables.min.js | 4 + .../Buttons-2.4.1/js/buttons.foundation.js | 122 + .../js/buttons.foundation.min.js | 4 + .../Buttons-2.4.1/js/buttons.html5.js | 1541 + .../Buttons-2.4.1/js/buttons.html5.min.js | 8 + .../Buttons-2.4.1/js/buttons.jqueryui.js | 98 + .../Buttons-2.4.1/js/buttons.jqueryui.min.js | 4 + .../Buttons-2.4.1/js/buttons.print.js | 242 + .../Buttons-2.4.1/js/buttons.print.min.js | 5 + .../Buttons-2.4.1/js/buttons.semanticui.js | 135 + .../js/buttons.semanticui.min.js | 4 + .../Buttons-2.4.1/js/dataTables.buttons.js | 2575 + .../js/dataTables.buttons.min.js | 4 + .../css/dataTables.bootstrap.css | 417 + .../css/dataTables.bootstrap.min.css | 1 + .../css/dataTables.bootstrap4.css | 424 + .../css/dataTables.bootstrap4.min.css | 1 + .../css/dataTables.bootstrap5.css | 437 + .../css/dataTables.bootstrap5.min.css | 5 + .../css/dataTables.bulma.css | 380 + .../css/dataTables.bulma.min.css | 3 + .../css/dataTables.dataTables.css | 0 .../css/dataTables.dataTables.min.css | 0 .../css/dataTables.foundation.css | 358 + .../css/dataTables.foundation.min.css | 1 + .../css/dataTables.jqueryui.css | 741 + .../css/dataTables.jqueryui.min.css | 1 + .../css/dataTables.semanticui.css | 349 + .../css/dataTables.semanticui.min.css | 1 + .../css/jquery.dataTables.css | 658 + .../css/jquery.dataTables.min.css | 1 + .../DataTables-1.13.5/images/sort_asc.png | Bin 0 -> 160 bytes .../images/sort_asc_disabled.png | Bin 0 -> 148 bytes .../DataTables-1.13.5/images/sort_both.png | Bin 0 -> 201 bytes .../DataTables-1.13.5/images/sort_desc.png | Bin 0 -> 158 bytes .../images/sort_desc_disabled.png | Bin 0 -> 146 bytes .../js/dataTables.bootstrap.js | 200 + .../js/dataTables.bootstrap.min.js | 4 + .../js/dataTables.bootstrap4.js | 202 + .../js/dataTables.bootstrap4.min.js | 4 + .../js/dataTables.bootstrap5.js | 212 + .../js/dataTables.bootstrap5.min.js | 4 + .../DataTables-1.13.5/js/dataTables.bulma.js | 214 + .../js/dataTables.bulma.min.js | 4 + .../js/dataTables.dataTables.js | 55 + .../js/dataTables.dataTables.min.js | 4 + .../js/dataTables.foundation.js | 200 + .../js/dataTables.foundation.min.js | 4 + .../js/dataTables.jqueryui.js | 95 + .../js/dataTables.jqueryui.min.js | 4 + .../js/dataTables.semanticui.js | 228 + .../js/dataTables.semanticui.min.js | 4 + .../DataTables-1.13.5/js/jquery.dataTables.js | 15706 ++ .../js/jquery.dataTables.min.js | 4 + .../css/dataTables.dateTime.css | 292 + .../css/dataTables.dateTime.min.css | 1 + .../DateTime-1.5.1/js/dataTables.dateTime.js | 1751 + .../js/dataTables.dateTime.min.js | 6 + .../css/fixedColumns.bootstrap.css | 57 + .../css/fixedColumns.bootstrap.min.css | 1 + .../css/fixedColumns.bootstrap4.css | 57 + .../css/fixedColumns.bootstrap4.min.css | 1 + .../css/fixedColumns.bootstrap5.css | 79 + .../css/fixedColumns.bootstrap5.min.css | 1 + .../css/fixedColumns.bulma.css | 50 + .../css/fixedColumns.bulma.min.css | 1 + .../css/fixedColumns.dataTables.css | 34 + .../css/fixedColumns.dataTables.min.css | 1 + .../css/fixedColumns.foundation.css | 38 + .../css/fixedColumns.foundation.min.css | 1 + .../css/fixedColumns.jqueryui.css | 52 + .../css/fixedColumns.jqueryui.min.css | 1 + .../css/fixedColumns.semanticui.css | 61 + .../css/fixedColumns.semanticui.min.css | 1 + .../js/dataTables.fixedColumns.js | 618 + .../js/dataTables.fixedColumns.min.js | 4 + .../js/fixedColumns.bootstrap.js | 58 + .../js/fixedColumns.bootstrap.min.js | 4 + .../js/fixedColumns.bootstrap4.js | 58 + .../js/fixedColumns.bootstrap4.min.js | 4 + .../js/fixedColumns.bootstrap5.js | 58 + .../js/fixedColumns.bootstrap5.min.js | 4 + .../js/fixedColumns.bulma.js | 58 + .../js/fixedColumns.bulma.min.js | 4 + .../js/fixedColumns.dataTables.js | 58 + .../js/fixedColumns.dataTables.min.js | 4 + .../js/fixedColumns.foundation.js | 58 + .../js/fixedColumns.foundation.min.js | 4 + .../js/fixedColumns.jqueryui.js | 58 + .../js/fixedColumns.jqueryui.min.js | 4 + .../js/fixedColumns.semanticui.js | 58 + .../js/fixedColumns.semanticui.min.js | 4 + .../css/fixedHeader.bootstrap.css | 16 + .../css/fixedHeader.bootstrap.min.css | 1 + .../css/fixedHeader.bootstrap4.css | 16 + .../css/fixedHeader.bootstrap4.min.css | 1 + .../css/fixedHeader.bootstrap5.css | 20 + .../css/fixedHeader.bootstrap5.min.css | 1 + .../css/fixedHeader.bulma.css | 16 + .../css/fixedHeader.bulma.min.css | 1 + .../css/fixedHeader.dataTables.css | 24 + .../css/fixedHeader.dataTables.min.css | 1 + .../css/fixedHeader.foundation.css | 16 + .../css/fixedHeader.foundation.min.css | 1 + .../css/fixedHeader.jqueryui.css | 14 + .../css/fixedHeader.jqueryui.min.css | 1 + .../css/fixedHeader.semanticui.css | 13 + .../css/fixedHeader.semanticui.min.css | 1 + .../js/dataTables.fixedHeader.js | 1112 + .../js/dataTables.fixedHeader.min.js | 4 + .../js/fixedHeader.bootstrap.js | 58 + .../js/fixedHeader.bootstrap.min.js | 4 + .../js/fixedHeader.bootstrap4.js | 58 + .../js/fixedHeader.bootstrap4.min.js | 4 + .../js/fixedHeader.bootstrap5.js | 58 + .../js/fixedHeader.bootstrap5.min.js | 4 + .../FixedHeader-3.4.0/js/fixedHeader.bulma.js | 58 + .../js/fixedHeader.bulma.min.js | 4 + .../js/fixedHeader.dataTables.js | 58 + .../js/fixedHeader.dataTables.min.js | 4 + .../js/fixedHeader.foundation.js | 58 + .../js/fixedHeader.foundation.min.js | 4 + .../js/fixedHeader.jqueryui.js | 58 + .../js/fixedHeader.jqueryui.min.js | 4 + .../js/fixedHeader.semanticui.js | 58 + .../js/fixedHeader.semanticui.min.js | 4 + .../clib/data-tables/JSZip-3.10.1/jszip.js | 11577 ++ .../data-tables/JSZip-3.10.1/jszip.min.js | 13 + .../Scroller-2.2.0/css/scroller.bootstrap.css | 49 + .../css/scroller.bootstrap.min.css | 1 + .../css/scroller.bootstrap4.css | 49 + .../css/scroller.bootstrap4.min.css | 1 + .../css/scroller.bootstrap5.css | 53 + .../css/scroller.bootstrap5.min.css | 1 + .../Scroller-2.2.0/css/scroller.bulma.css | 45 + .../Scroller-2.2.0/css/scroller.bulma.min.css | 1 + .../css/scroller.dataTables.css | 45 + .../css/scroller.dataTables.min.css | 1 + .../css/scroller.foundation.css | 17 + .../css/scroller.foundation.min.css | 1 + .../Scroller-2.2.0/css/scroller.jqueryui.css | 45 + .../css/scroller.jqueryui.min.css | 1 + .../css/scroller.semanticui.css | 45 + .../css/scroller.semanticui.min.css | 1 + .../Scroller-2.2.0/js/dataTables.scroller.js | 1328 + .../js/dataTables.scroller.min.js | 4 + .../Scroller-2.2.0/js/scroller.bootstrap.js | 58 + .../js/scroller.bootstrap.min.js | 4 + .../Scroller-2.2.0/js/scroller.bootstrap4.js | 58 + .../js/scroller.bootstrap4.min.js | 4 + .../Scroller-2.2.0/js/scroller.bootstrap5.js | 58 + .../js/scroller.bootstrap5.min.js | 4 + .../Scroller-2.2.0/js/scroller.bulma.js | 58 + .../Scroller-2.2.0/js/scroller.bulma.min.js | 4 + .../Scroller-2.2.0/js/scroller.dataTables.js | 58 + .../js/scroller.dataTables.min.js | 4 + .../Scroller-2.2.0/js/scroller.foundation.js | 58 + .../js/scroller.foundation.min.js | 4 + .../Scroller-2.2.0/js/scroller.jqueryui.js | 58 + .../js/scroller.jqueryui.min.js | 4 + .../Scroller-2.2.0/js/scroller.semanticui.js | 58 + .../js/scroller.semanticui.min.js | 4 + .../SearchBuilder-1.5.0/css/common.scss | 205 + .../css/dataTables.dateTime.scss | 244 + .../css/searchBuilder.bootstrap.css | 193 + .../css/searchBuilder.bootstrap.min.css | 1 + .../css/searchBuilder.bootstrap4.css | 203 + .../css/searchBuilder.bootstrap4.min.css | 1 + .../css/searchBuilder.bootstrap5.css | 210 + .../css/searchBuilder.bootstrap5.min.css | 1 + .../css/searchBuilder.bulma.css | 204 + .../css/searchBuilder.bulma.min.css | 1 + .../css/searchBuilder.dataTables.css | 260 + .../css/searchBuilder.dataTables.min.css | 1 + .../css/searchBuilder.foundation.css | 208 + .../css/searchBuilder.foundation.min.css | 1 + .../css/searchBuilder.jqueryui.css | 192 + .../css/searchBuilder.jqueryui.min.css | 1 + .../css/searchBuilder.semanticui.css | 238 + .../css/searchBuilder.semanticui.min.css | 1 + .../js/dataTables.searchBuilder.js | 3831 + .../js/dataTables.searchBuilder.min.js | 4 + .../js/searchBuilder.bootstrap.js | 74 + .../js/searchBuilder.bootstrap.min.js | 4 + .../js/searchBuilder.bootstrap4.js | 74 + .../js/searchBuilder.bootstrap4.min.js | 4 + .../js/searchBuilder.bootstrap5.js | 76 + .../js/searchBuilder.bootstrap5.min.js | 4 + .../js/searchBuilder.bulma.js | 72 + .../js/searchBuilder.bulma.min.js | 4 + .../js/searchBuilder.dataTables.js | 58 + .../js/searchBuilder.dataTables.min.js | 4 + .../js/searchBuilder.foundation.js | 74 + .../js/searchBuilder.foundation.min.js | 4 + .../js/searchBuilder.jqueryui.js | 74 + .../js/searchBuilder.jqueryui.min.js | 4 + .../js/searchBuilder.semanticui.js | 86 + .../js/searchBuilder.semanticui.min.js | 4 + .../css/searchPanes.bootstrap.css | 340 + .../css/searchPanes.bootstrap.min.css | 1 + .../css/searchPanes.bootstrap4.css | 337 + .../css/searchPanes.bootstrap4.min.css | 1 + .../css/searchPanes.bootstrap5.css | 396 + .../css/searchPanes.bootstrap5.min.css | 1 + .../css/searchPanes.bulma.css | 323 + .../css/searchPanes.bulma.min.css | 1 + .../css/searchPanes.dataTables.css | 413 + .../css/searchPanes.dataTables.min.css | 1 + .../css/searchPanes.foundation.css | 353 + .../css/searchPanes.foundation.min.css | 1 + .../css/searchPanes.jqueryui.css | 441 + .../css/searchPanes.jqueryui.min.css | 1 + .../css/searchPanes.semanticui.css | 400 + .../css/searchPanes.semanticui.min.css | 1 + .../js/dataTables.searchPanes.js | 3452 + .../js/dataTables.searchPanes.min.js | 4 + .../js/searchPanes.bootstrap.js | 82 + .../js/searchPanes.bootstrap.min.js | 4 + .../js/searchPanes.bootstrap4.js | 86 + .../js/searchPanes.bootstrap4.min.js | 4 + .../js/searchPanes.bootstrap5.js | 82 + .../js/searchPanes.bootstrap5.min.js | 4 + .../SearchPanes-2.2.0/js/searchPanes.bulma.js | 70 + .../js/searchPanes.bulma.min.js | 4 + .../js/searchPanes.dataTables.js | 58 + .../js/searchPanes.dataTables.min.js | 4 + .../js/searchPanes.foundation.js | 80 + .../js/searchPanes.foundation.min.js | 4 + .../js/searchPanes.jqueryui.js | 70 + .../js/searchPanes.jqueryui.min.js | 4 + .../js/searchPanes.semanticui.js | 77 + .../js/searchPanes.semanticui.min.js | 4 + .../wwwroot/clib/data-tables/datatables.css | 1663 + .../wwwroot/clib/data-tables/datatables.js | 118123 +++++++++++++++ .../clib/data-tables/datatables.min.css | 36 + .../clib/data-tables/datatables.min.js | 114 + .../clib/data-tables/pdfmake-0.2.7/pdfmake.js | 73862 +++++++++ .../data-tables/pdfmake-0.2.7/pdfmake.min.js | 3 + .../data-tables/pdfmake-0.2.7/vfs_fonts.js | 6 + Web/Resgrid.WebCore/wwwroot/favicon.ico | Bin 1150 -> 15406 bytes .../wwwroot/images/android-chrome-192x192.png | Bin 0 -> 15873 bytes .../wwwroot/images/android-chrome-512x512.png | Bin 0 -> 43076 bytes .../wwwroot/images/apple-touch-icon.png | Bin 7473 -> 14858 bytes .../wwwroot/images/favicon-16x16.png | Bin 0 -> 746 bytes .../wwwroot/images/favicon-32x32.png | Bin 0 -> 1719 bytes .../wwwroot/images/flags/select2/ak.png | Bin 0 -> 2212 bytes .../wwwroot/images/flags/select2/ca.png | Bin 0 -> 7159 bytes .../wwwroot/images/flags/select2/hi.png | Bin 0 -> 3170 bytes .../wwwroot/images/flags/select2/nv.png | Bin 0 -> 4268 bytes .../wwwroot/images/flags/select2/or.png | Bin 0 -> 9530 bytes .../wwwroot/images/flags/select2/wa.png | Bin 0 -> 45254 bytes .../resgrid.dispatch.addArchivedCall.js | 191 +- .../dispatch/resgrid.dispatch.editcall.js | 216 +- .../dispatch/resgrid.dispatch.newcall.js | 1090 +- .../dispatch/resgrid.dispatch.viewcall.js | 33 +- .../internal/mapping/resgrid.mapping.index.js | 169 +- .../personnel/resgrid.personnel.index.js | 276 +- .../wwwroot/js/app/internal/resgrid.user.js | 65 +- .../app/internal/units/resgrid.units.index.js | 362 +- Web/Resgrid.WebCore/wwwroot/site.webmanifest | 19 + .../Commands/SystemSqlQueueCommand.cs | 19 + Workers/Resgrid.Workers.Console/Program.cs | 10 +- .../Tasks/DispatchScheduledCallsTask.cs | 2 +- .../Tasks/SystemSqlQueueTask.cs | 58 + .../Logic/AuditQueueLogic.cs | 27 +- .../Logic/CallBroadcast.cs | 340 +- .../Logic/SystemQueueLogic.cs | 2 +- 625 files changed, 281961 insertions(+), 8454 deletions(-) create mode 100644 .github/workflows/auto-approve.yml create mode 100644 Core/Resgrid.Localization/Account/Login.cs create mode 100644 Core/Resgrid.Localization/Account/Login.en.resx create mode 100644 Core/Resgrid.Localization/Account/Login.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Calendar/Calendar.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Calendar/Calendar.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Calendar/Calendar.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.cs create mode 100644 Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Department/Department.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Department/Department.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Department/Department.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Department/Department.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Dispatch/Call.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Home/EditProfile.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Home/EditProfile.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Home/EditProfile.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Logs/Logs.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Logs/Logs.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Logs/Logs.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Messages/Messages.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Messages/Messages.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Messages/Messages.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Notes/Note.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Notes/Note.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Notes/Note.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Personnel/Person.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Personnel/Person.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Personnel/Person.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Profile/Profile.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Profile/Profile.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Profile/Profile.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Subscription/Subscription.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Subscription/Subscription.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Subscription/Subscription.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Subscription/Subscription.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Units/Units.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Units/Units.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Units/Units.es.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Voice/Voice.cs create mode 100644 Core/Resgrid.Localization/Areas/User/Voice/Voice.en.resx create mode 100644 Core/Resgrid.Localization/Areas/User/Voice/Voice.es.resx create mode 100644 Core/Resgrid.Localization/Common.cs create mode 100644 Core/Resgrid.Localization/Common.en.resx create mode 100644 Core/Resgrid.Localization/Common.es.resx create mode 100644 Core/Resgrid.Localization/Resgrid.Localization.csproj create mode 100644 Core/Resgrid.Localization/SupportedLocales.cs create mode 100644 Core/Resgrid.Model/CallReference.cs create mode 100644 Core/Resgrid.Model/DepartmentAudio.cs create mode 100644 Core/Resgrid.Model/DepartmentSuppressStaffingInfo.cs create mode 100644 Core/Resgrid.Model/DepartmentVoiceUtilization.cs create mode 100644 Core/Resgrid.Model/Repositories/ICallReferencesRepository.cs create mode 100644 Core/Resgrid.Model/Repositories/IDeleteRepository.cs create mode 100644 Core/Resgrid.Model/Repositories/IDepartmentAudioRepository.cs create mode 100644 Providers/Resgrid.Providers.Email/Template/DeleteDepartment.html create mode 100644 Providers/Resgrid.Providers.Migrations/Migrations/M0020_AddingLangToUPAndSystemQ.cs create mode 100644 Providers/Resgrid.Providers.Migrations/Migrations/M0021_AddingCallReferences.cs create mode 100644 Providers/Resgrid.Providers.Migrations/Migrations/M0022_AddingQuantityForPlanAddon.cs create mode 100644 Providers/Resgrid.Providers.Migrations/Migrations/M0023_AddingPlanAddonFor10Pack.cs create mode 100644 Providers/Resgrid.Providers.Migrations/Migrations/M0024_AddingDepartmentAudio.cs create mode 100644 Providers/Resgrid.Providers.Migrations/Migrations/M0025_AddingTypeToDepartmentAudio.cs create mode 100644 Providers/Resgrid.Providers.Voip/LiveKit/LiveKitAccessToken.cs create mode 100644 Providers/Resgrid.Providers.Voip/LiveKit/LiveKitGrant.cs create mode 100644 Providers/Resgrid.Providers.Voip/LiveKit/LiveKitVideoGrant.cs create mode 100644 Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitAccessTokenOptions.cs create mode 100644 Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitParticipantInfo.cs create mode 100644 Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitParticipants.cs create mode 100644 Providers/Resgrid.Providers.Voip/LiveKitProvider.cs create mode 100644 Repositories/Resgrid.Repositories.DataRepository/CallReferencesRepository.cs create mode 100644 Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs create mode 100644 Repositories/Resgrid.Repositories.DataRepository/DepartmentAudioRepository.cs create mode 100644 Repositories/Resgrid.Repositories.DataRepository/Queries/Calls/SelectAllCallReferencesBySourceCallIdQuery.cs create mode 100644 Repositories/Resgrid.Repositories.DataRepository/Queries/Calls/SelectAllCallReferencesByTargetCallIdQuery.cs create mode 100644 Web/Resgrid.Web.ServicesCore/Middleware/ApiTelemetryInitializer.cs create mode 100644 Web/Resgrid.Web.ServicesCore/Models/v4/Voice/CanConnectToVoiceSessionResult.cs create mode 100644 Web/Resgrid.Web.ServicesCore/Models/v4/Voice/DepartmentAudioResult.cs create mode 100644 Web/Resgrid.WebCore/Areas/User/Apps/src/app/componetns/omnibar/omnibar.component.css create mode 100644 Web/Resgrid.WebCore/Areas/User/Apps/src/app/componetns/omnibar/omnibar.component.html create mode 100644 Web/Resgrid.WebCore/Areas/User/Apps/src/app/componetns/omnibar/omnibar.component.ts create mode 100644 Web/Resgrid.WebCore/Areas/User/Controllers/SearchController.cs create mode 100644 Web/Resgrid.WebCore/Areas/User/Models/Departments/DeleteDepartmentView.cs create mode 100644 Web/Resgrid.WebCore/Areas/User/Models/Dispatch/CallSelectListJson.cs create mode 100644 Web/Resgrid.WebCore/Areas/User/Models/Search/SearchResultJson.cs create mode 100644 Web/Resgrid.WebCore/Areas/User/Models/Voice/NewAudioStreamModel.cs create mode 100644 Web/Resgrid.WebCore/Areas/User/Views/Department/DeleteDepartment.cshtml create mode 100644 Web/Resgrid.WebCore/Areas/User/Views/Voice/EditAudio.cshtml create mode 100644 Web/Resgrid.WebCore/Areas/User/Views/Voice/NewAudio.cshtml create mode 100644 Web/Resgrid.WebCore/Middleware/WebTelemetryInitializer.cs create mode 100644 Web/Resgrid.WebCore/Properties/ServiceDependencies/local/appInsights1.arm.json create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.bootstrap.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.bootstrap.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.bootstrap4.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.bootstrap4.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.bootstrap5.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.bootstrap5.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.bulma.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.bulma.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.dataTables.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.dataTables.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.foundation.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.foundation.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.jqueryui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.jqueryui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.semanticui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/buttons.semanticui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/common.scss create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/css/mixins.scss create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.bootstrap.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.bootstrap.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.bootstrap4.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.bootstrap4.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.bootstrap5.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.bootstrap5.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.bulma.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.bulma.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.colVis.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.colVis.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.dataTables.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.dataTables.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.foundation.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.foundation.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.html5.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.html5.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.jqueryui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.jqueryui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.print.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.print.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.semanticui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/buttons.semanticui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/dataTables.buttons.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Buttons-2.4.1/js/dataTables.buttons.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.bootstrap.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.bootstrap.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.bootstrap4.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.bootstrap4.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.bootstrap5.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.bootstrap5.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.bulma.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.bulma.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.dataTables.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.dataTables.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.foundation.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.foundation.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.jqueryui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.jqueryui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.semanticui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/dataTables.semanticui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/jquery.dataTables.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/css/jquery.dataTables.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/images/sort_asc.png create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/images/sort_asc_disabled.png create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/images/sort_both.png create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/images/sort_desc.png create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/images/sort_desc_disabled.png create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.bootstrap.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.bootstrap.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.bootstrap4.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.bootstrap4.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.bootstrap5.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.bootstrap5.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.bulma.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.bulma.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.dataTables.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.dataTables.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.foundation.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.foundation.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.jqueryui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.jqueryui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.semanticui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/dataTables.semanticui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/jquery.dataTables.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DataTables-1.13.5/js/jquery.dataTables.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DateTime-1.5.1/css/dataTables.dateTime.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DateTime-1.5.1/css/dataTables.dateTime.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DateTime-1.5.1/js/dataTables.dateTime.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/DateTime-1.5.1/js/dataTables.dateTime.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.bootstrap.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.bootstrap.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.bootstrap4.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.bootstrap4.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.bootstrap5.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.bootstrap5.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.bulma.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.bulma.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.dataTables.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.dataTables.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.foundation.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.foundation.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.jqueryui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.jqueryui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.semanticui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/css/fixedColumns.semanticui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/dataTables.fixedColumns.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/dataTables.fixedColumns.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.bootstrap.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.bootstrap.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.bootstrap4.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.bootstrap4.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.bootstrap5.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.bootstrap5.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.bulma.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.bulma.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.dataTables.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.dataTables.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.foundation.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.foundation.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.jqueryui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.jqueryui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.semanticui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedColumns-4.3.0/js/fixedColumns.semanticui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.bootstrap.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.bootstrap.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.bootstrap4.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.bootstrap4.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.bootstrap5.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.bootstrap5.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.bulma.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.bulma.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.dataTables.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.dataTables.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.foundation.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.foundation.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.jqueryui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.jqueryui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.semanticui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/css/fixedHeader.semanticui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/dataTables.fixedHeader.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/dataTables.fixedHeader.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.bootstrap.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.bootstrap.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.bootstrap4.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.bootstrap4.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.bootstrap5.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.bootstrap5.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.bulma.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.bulma.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.dataTables.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.dataTables.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.foundation.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.foundation.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.jqueryui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.jqueryui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.semanticui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/FixedHeader-3.4.0/js/fixedHeader.semanticui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/JSZip-3.10.1/jszip.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/JSZip-3.10.1/jszip.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.bootstrap.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.bootstrap.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.bootstrap4.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.bootstrap4.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.bootstrap5.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.bootstrap5.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.bulma.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.bulma.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.dataTables.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.dataTables.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.foundation.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.foundation.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.jqueryui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.jqueryui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.semanticui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/css/scroller.semanticui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/dataTables.scroller.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/dataTables.scroller.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.bootstrap.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.bootstrap.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.bootstrap4.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.bootstrap4.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.bootstrap5.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.bootstrap5.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.bulma.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.bulma.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.dataTables.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.dataTables.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.foundation.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.foundation.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.jqueryui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.jqueryui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.semanticui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/Scroller-2.2.0/js/scroller.semanticui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/common.scss create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/dataTables.dateTime.scss create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.bootstrap.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.bootstrap.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.bootstrap4.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.bootstrap4.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.bootstrap5.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.bootstrap5.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.bulma.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.bulma.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.dataTables.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.dataTables.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.foundation.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.foundation.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.jqueryui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.jqueryui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.semanticui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/css/searchBuilder.semanticui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/dataTables.searchBuilder.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/dataTables.searchBuilder.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.bootstrap.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.bootstrap.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.bootstrap4.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.bootstrap4.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.bootstrap5.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.bootstrap5.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.bulma.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.bulma.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.dataTables.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.dataTables.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.foundation.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.foundation.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.jqueryui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.jqueryui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.semanticui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchBuilder-1.5.0/js/searchBuilder.semanticui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.bootstrap.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.bootstrap.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.bootstrap4.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.bootstrap4.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.bootstrap5.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.bootstrap5.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.bulma.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.bulma.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.dataTables.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.dataTables.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.foundation.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.foundation.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.jqueryui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.jqueryui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.semanticui.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/css/searchPanes.semanticui.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/dataTables.searchPanes.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/dataTables.searchPanes.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.bootstrap.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.bootstrap.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.bootstrap4.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.bootstrap4.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.bootstrap5.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.bootstrap5.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.bulma.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.bulma.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.dataTables.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.dataTables.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.foundation.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.foundation.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.jqueryui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.jqueryui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.semanticui.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/SearchPanes-2.2.0/js/searchPanes.semanticui.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/datatables.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/datatables.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/datatables.min.css create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/datatables.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/pdfmake-0.2.7/pdfmake.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/pdfmake-0.2.7/pdfmake.min.js create mode 100644 Web/Resgrid.WebCore/wwwroot/clib/data-tables/pdfmake-0.2.7/vfs_fonts.js create mode 100644 Web/Resgrid.WebCore/wwwroot/images/android-chrome-192x192.png create mode 100644 Web/Resgrid.WebCore/wwwroot/images/android-chrome-512x512.png create mode 100644 Web/Resgrid.WebCore/wwwroot/images/favicon-16x16.png create mode 100644 Web/Resgrid.WebCore/wwwroot/images/favicon-32x32.png create mode 100644 Web/Resgrid.WebCore/wwwroot/images/flags/select2/ak.png create mode 100644 Web/Resgrid.WebCore/wwwroot/images/flags/select2/ca.png create mode 100644 Web/Resgrid.WebCore/wwwroot/images/flags/select2/hi.png create mode 100644 Web/Resgrid.WebCore/wwwroot/images/flags/select2/nv.png create mode 100644 Web/Resgrid.WebCore/wwwroot/images/flags/select2/or.png create mode 100644 Web/Resgrid.WebCore/wwwroot/images/flags/select2/wa.png create mode 100644 Web/Resgrid.WebCore/wwwroot/site.webmanifest create mode 100644 Workers/Resgrid.Workers.Console/Commands/SystemSqlQueueCommand.cs create mode 100644 Workers/Resgrid.Workers.Console/Tasks/SystemSqlQueueTask.cs diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml new file mode 100644 index 00000000..04273144 --- /dev/null +++ b/.github/workflows/auto-approve.yml @@ -0,0 +1,26 @@ +name: Auto approve + +on: + issue_comment: + types: + - created + +jobs: + auto-approve: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/github-script@v6 + name: Approve LGTM Review + if: github.actor == 'ucswift' && contains(github.event.comment.body, 'Approve') + with: + script: | + github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + review_id: 1, + event: 'APPROVE', + body: 'This PR is approved.' + }) \ No newline at end of file diff --git a/Core/Resgrid.Config/ExternalErrorConfig.cs b/Core/Resgrid.Config/ExternalErrorConfig.cs index 9f150794..56d8731b 100644 --- a/Core/Resgrid.Config/ExternalErrorConfig.cs +++ b/Core/Resgrid.Config/ExternalErrorConfig.cs @@ -15,10 +15,18 @@ public static class ExternalErrorConfig #endregion Elk Settings #region Sentry Settings - public static string ExternalErrorServiceUrl = ""; + public static string ExternalErrorServiceUrlForApi = ""; public static string ExternalErrorServiceUrlForWebsite = ""; public static string ExternalErrorServiceUrlForWebjobs = ""; + public static string ExternalErrorServiceUrlForEventing = ""; + public static double SentryPerfSampleRate = 1.0; #endregion Sentry Settings + + #region Application Insights Settings + public static bool ApplicationInsightsEnabled = false; + public static string ApplicationInsightsInstrumentationKey = ""; + public static string ApplicationInsightsConnectionString = ""; + #endregion Application Insights Settings } public enum ErrorLoggerTypes diff --git a/Core/Resgrid.Config/MappingConfig.cs b/Core/Resgrid.Config/MappingConfig.cs index 17873a02..fbc40526 100644 --- a/Core/Resgrid.Config/MappingConfig.cs +++ b/Core/Resgrid.Config/MappingConfig.cs @@ -17,6 +17,11 @@ public static class MappingConfig public static string LoqateApiUrl = "https://saas.loqate.com"; public static string LoqateApiKey = ""; + /*********************************** + * OpenWeatherApi Keys + ***********************************/ + public static string BigBoardOpenWeatherApiKey = ""; + /*********************************** * Leaflet OSM Keys (used for Mapping and Map display) ***********************************/ diff --git a/Core/Resgrid.Config/VoipConfig.cs b/Core/Resgrid.Config/VoipConfig.cs index 321ef811..ecbe33cb 100644 --- a/Core/Resgrid.Config/VoipConfig.cs +++ b/Core/Resgrid.Config/VoipConfig.cs @@ -21,6 +21,11 @@ public static class VoipConfig public static string OpenViduUrl = ""; public static string OpenViduSecret = ""; + + public static string LiveKitServerApiUrl = ""; + public static string LiveKitServerUrl = ""; + public static string LiveKitServerApiKey = ""; + public static string LiveKitServerApiSecret = ""; } /// @@ -29,6 +34,7 @@ public static class VoipConfig public enum VoipProviderTypes { Kazoo = 0, - OpenVidu = 1 + OpenVidu = 1, + LiveKit = 2 } } diff --git a/Core/Resgrid.Framework/DateTimeHelpers.cs b/Core/Resgrid.Framework/DateTimeHelpers.cs index 2f8b72c2..9b518825 100644 --- a/Core/Resgrid.Framework/DateTimeHelpers.cs +++ b/Core/Resgrid.Framework/DateTimeHelpers.cs @@ -107,22 +107,37 @@ public static DateTime ConvertToUtc(DateTime dateTime, string timeZone) //LocalDateTime localDateTime = new LocalDateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, dateTime.Second); //ZonedDateTime zonedDateTime = new ZonedDateTime(localDateTime, DateTimeZoneProviders.Tzdb[timeZone]); - var ianaTz = TZConvert.WindowsToIana(timeZone); + if (!String.IsNullOrWhiteSpace(timeZone)) + { + var ianaTz = TZConvert.WindowsToIana(timeZone); + + var localTime = LocalDateTime.FromDateTime(dateTime); + var zonedDateTime = localTime.InZoneStrictly(DateTimeZoneProviders.Tzdb[ianaTz]); - var localTime = LocalDateTime.FromDateTime(dateTime); - var zonedDateTime = localTime.InZoneStrictly(DateTimeZoneProviders.Tzdb[ianaTz]); + return zonedDateTime.ToDateTimeUtc(); + } - return zonedDateTime.ToDateTimeUtc(); + return dateTime.ToUniversalTime(); } public static DateTime GetLocalDateTime(DateTime dateTime, string timeZone) { - var ianaTz = TZConvert.WindowsToIana(timeZone); + if (!String.IsNullOrWhiteSpace(timeZone)) + { + var ianaTz = TZConvert.WindowsToIana(timeZone); - var localTime = LocalDateTime.FromDateTime(dateTime); - var zonedDateTime = localTime.InZoneStrictly(DateTimeZoneProviders.Tzdb[ianaTz]); + //var localTime = LocalDateTime.FromDateTime(dateTime); + var TzdbTZ = DateTimeZoneProviders.Tzdb[ianaTz]; + //var zonedDateTime = localTime.InZoneStrictly(TzdbTZ); - return zonedDateTime.LocalDateTime.ToDateTimeUnspecified(); + var instant = Instant.FromDateTimeUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)); + var result = instant.InZone(TzdbTZ).ToDateTimeUnspecified(); + return result; + + //return zonedDateTime.LocalDateTime.ToDateTimeUnspecified(); + } + + return dateTime; } public static TimeZoneInfo CreateTimeZoneInfo(string timeZone) diff --git a/Core/Resgrid.Framework/LocationHelpers.cs b/Core/Resgrid.Framework/LocationHelpers.cs index 6dbb704c..09665106 100644 --- a/Core/Resgrid.Framework/LocationHelpers.cs +++ b/Core/Resgrid.Framework/LocationHelpers.cs @@ -62,5 +62,9 @@ public static double ConvertDegreeAngleToDouble(string point) return (degrees + minutes + seconds) * multiplier; } + + public static bool IsValidLatitude(string latitude) => double.TryParse(latitude, out var l) && -90 <= l && l <= 90; + + public static bool IsValidLongitude(string longitude) => double.TryParse(longitude, out var l) && -180 <= l && l <= 180; } } diff --git a/Core/Resgrid.Framework/Logging.cs b/Core/Resgrid.Framework/Logging.cs index 7f99e3a9..b6f95fa5 100644 --- a/Core/Resgrid.Framework/Logging.cs +++ b/Core/Resgrid.Framework/Logging.cs @@ -25,7 +25,7 @@ public static void Initialize(string key) { if (SystemBehaviorConfig.ErrorLoggerType == ErrorLoggerTypes.Sentry) { - string dsn = ExternalErrorConfig.ExternalErrorServiceUrl; + string dsn = ExternalErrorConfig.ExternalErrorServiceUrlForApi; if (!String.IsNullOrWhiteSpace(key)) dsn = key; @@ -39,6 +39,7 @@ public static void Initialize(string key) o.Dsn = dsn; o.AttachStacktrace = true; o.SendDefaultPii = true; + o.TracesSampleRate = ExternalErrorConfig.SentryPerfSampleRate; o.Environment = ExternalErrorConfig.Environment; o.Release = Assembly.GetEntryAssembly().GetName().Version.ToString(); }).CreateLogger(); diff --git a/Core/Resgrid.Framework/StringHelpers.cs b/Core/Resgrid.Framework/StringHelpers.cs index f7c1968b..f5fe0915 100644 --- a/Core/Resgrid.Framework/StringHelpers.cs +++ b/Core/Resgrid.Framework/StringHelpers.cs @@ -206,5 +206,17 @@ public static string TrimLastCharacter(this String str) return str.TrimEnd(str[str.Length - 1]); } } + + public static string Base64Encode(string plainText) + { + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); + return System.Convert.ToBase64String(plainTextBytes); + } + + public static string Base64Decode(string base64EncodedData) + { + var base64EncodedBytes = System.Convert.FromBase64String(base64EncodedData); + return System.Text.Encoding.UTF8.GetString(base64EncodedBytes); + } } } diff --git a/Core/Resgrid.Localization/Account/Login.cs b/Core/Resgrid.Localization/Account/Login.cs new file mode 100644 index 00000000..a96fd156 --- /dev/null +++ b/Core/Resgrid.Localization/Account/Login.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Account +{ + public class Login + { + } +} diff --git a/Core/Resgrid.Localization/Account/Login.en.resx b/Core/Resgrid.Localization/Account/Login.en.resx new file mode 100644 index 00000000..63f91629 --- /dev/null +++ b/Core/Resgrid.Localization/Account/Login.en.resx @@ -0,0 +1,255 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Completed Invite + + + Complete Invite + + + Supply the information to create your own Resgrid account and join + + + The information you are supplying is used to create your account in Resgrid. If you already have an account your existing account will be linked with the department as part of this invite. + + + If you are having trouble completing the invite or are running into issues, please reach out to the Administrator of the department for the invite you are trying to complete for assistance. They can manually add your account if necessary. + + + Confirm Password + + + Create an account + + + Department Name + + + Do not have an account? + + + Email Address + + + First Name + + + Forgot your username or password? + + + Go Back + + + Home + + + It looks this invite has already been completed. If this is in error contact your department or origination to get another invite, else you can log in with your username or password. + + + Resgrid Invite has already been completed + + + Last Name + + + Locked out + + + This account has been locked out, please try and log in again in 30 minutes. Please note support cannot unlock an account for you. + + + Log In + + + Enter your username and password to login. + + + If your department has already signed up ask that departments admin for an invite. + + + If your department has not yet signed up for Resgrid you can create a new account which will create the department for you. + + + Maintenance is performed weekly on Saturday starting at 2000 hours Pacific. + + + Log On + + + We're sorry but we were unable to locate your invite to Resgrid. Try having your department or organization send another one. If you already have a Resgrid account you can log in below. + + + Invite Missing + + + Unable to find your Resgrid Invite + + + Notice + + + Password + + + Passwords must be 8 characters or longer and include a digit (number), an uppercase and lowercase letter + + + privacy policy + + + If your email address is valid we have reset your password and sent your username and new password to the email address supplied. Email delivery can be delayed up to 15 minutes, <b>also check your spam or junk areas in your email client if you email doesn't arrive</b> by then. + + + Supply the email address you used with your Resgrid account. We will then send you an email to that address with your username and new password to log into the system. + + + Recover Account + + + Register + + + Registering for Resgrid is as easy as 1,2,3! + + + Have one person from your org fill out this form + + + Once Signed up, you can invite or add people from the Personnel page inside the app + + + Thats it! Only a single person needs to Register the organization + + + If someone in your organization <b>already has an account with Resgrid</b> have them invite you to their department instead of creating a new one, if you want to use Resgrid with them. + + + Resgister + + + By signing up, you accept + + + terms of use + + + Username + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Account/Login.es.resx b/Core/Resgrid.Localization/Account/Login.es.resx new file mode 100644 index 00000000..15a18815 --- /dev/null +++ b/Core/Resgrid.Localization/Account/Login.es.resx @@ -0,0 +1,255 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Invitación completada + + + Invitación completa + + + Proporcione la información para crear su propia cuenta Resgrid y unirse + + + La información que proporciona se utiliza para crear su cuenta en Resgrid. Si ya tiene una cuenta, su cuenta existente se vinculará con el departamento como parte de esta invitación. + + + Si tiene problemas para completar la invitación o tiene problemas, comuníquese con el administrador del departamento de la invitación que está tratando de completar para obtener ayuda. Pueden agregar manualmente su cuenta si es necesario. + + + confirmar Contraseña + + + Crea una cuenta + + + Nombre de Departamento + + + ¿No tiene una cuenta? + + + Dirección de correo electrónico + + + Nombre de pila + + + ¿Ha olvidado su nombre de usuario o contraseña? + + + Regresa + + + Hogar + + + Parece que esta invitación ya se ha completado. Si se trata de un error, comuníquese con su departamento u origen para obtener otra invitación; de lo contrario, puede iniciar sesión con su nombre de usuario o contraseña. + + + La invitación a Resgrid ya se completó + + + Apellido + + + Bloqueado + + + Esta cuenta ha sido bloqueada, intente iniciar sesión nuevamente en 30 minutos. Tenga en cuenta que el soporte no puede desbloquearle una cuenta. + + + Acceso + + + Ingrese su nombre de usuario y contraseña para iniciar sesión. + + + Si su departamento ya se ha registrado, solicite una invitación al administrador del departamento. + + + Si su departamento aún no se ha registrado en Resgrid, puede crear una nueva cuenta que creará el departamento por usted. + + + El mantenimiento se realiza semanalmente los sábados a partir de las 2000 horas del Pacífico. + + + Acceder + + + Lo sentimos, pero no pudimos localizar su invitación a Resgrid. Intente que su departamento u organización envíe otro. Si ya tiene una cuenta de Resgrid, puede iniciar sesión a continuación. + + + Falta la invitación + + + No se puede encontrar su invitación de Resgrid + + + Aviso + + + Contraseña + + + Las contraseñas deben tener 8 caracteres o más e incluir un dígito (número), una letra mayúscula y minúscula + + + política de privacidad + + + Si su dirección de correo electrónico es válida, hemos restablecido su contraseña y le hemos enviado su nombre de usuario y nueva contraseña a la dirección de correo electrónico proporcionada. La entrega del correo electrónico se puede retrasar hasta 15 minutos, <b>verifique también las áreas de spam o basura en su cliente de correo electrónico si su correo electrónico no llega</b> para entonces. + + + Proporcione la dirección de correo electrónico que utilizó con su cuenta de Resgrid. Luego le enviaremos un correo electrónico a esa dirección con su nombre de usuario y nueva contraseña para iniciar sesión en el sistema. + + + Recuperar cuenta + + + Registro + + + ¡Registrarse en Resgrid es tan fácil como 1,2,3! + + + Pida a una persona de su organización que complete este formulario + + + Una vez registrado, puede invitar o agregar personas desde la página de Personal dentro de la aplicación + + + ¡Eso es todo! Solo una persona necesita registrar la organización + + + Si alguien en su organización <b>ya tiene una cuenta con Resgrid</b>, pídale que lo invite a su departamento en lugar de crear uno nuevo, si desea usar Resgrid con ellos. + + + Registrarse + + + Al registrarte, aceptas + + + condiciones de uso + + + Nombre de usuario + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.cs b/Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.cs new file mode 100644 index 00000000..c4808ca4 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Account +{ + public class DeleteAccount + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.en.resx b/Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.en.resx new file mode 100644 index 00000000..24ab235e --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Account/DeleteAccount.en.resx @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Check the checkbox below to confirm you understand the above and wish to delete you account. + + + Check Box to Confirm + + + Delete Your Account + + + Here you can delete your personal account in the Resgrid system. If you are an Account Owner in any department in the Resgrid system, you have to delete your account by deleting that department or you need to transfer ownership of that Department to someone else via editing the Department Settings. + + + If you wish to leave a department, it’s recommended that the Department admins delete the account for you via the Personnel list. But if that’s not possible you can delete your own account here. + + + Please note, data supplied to Resgrid for a department is owned by that department and could be subject to data retention policies and laws governing the jurisdiction of the department. Deleting your account here will clear your PII (Personally Identifiable Information) and delete your login but won’t clear all data. You will need to make a request to the department you were apart of for additional if you want all data cleared. + + + You are unable to delete your account because you are a Department Owner in a department. If you wish to delete the department you will need to access that though the Department Settings page. If you want to retain the department please choose another person as the Department Owner and then try and delete your account again. + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.cs b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.cs new file mode 100644 index 00000000..b814e8b5 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Calendar +{ + public class Calendar + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.en.resx b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.en.resx new file mode 100644 index 00000000..80a36e06 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.en.resx @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + All Day + + + Calendar + + + Event Detail + + + WARNING: This will permanently delete this type. Are you sure you want to delete the type + + + Calendar Types + + + Days of the Week + + + Delete Event + + + Delete Event and Occurrences + + + Edit Calendar Type + + + Edit Calendar Entry + + + This is the recurrence parent item. If you check the "Apply to all" checkbox that will update all the recurrence items with the data from this item. If you delete this item all child recurrence items will also be deleted. + + + Edit Event + + + Leave blank (empty) to never Expire or End + + + End On + + + Entities to Notify + + + first + + + forth + + + last + + + Manage Types + + + Manage Calendar Types + + + New Calendar Entry + + + New Calendar Type + + + New Entry + + + Add a new calendar entry + + + No Custom Types + + + None + + + No Note + + + No Upcoming Items + + + This is the recurrence child item. You cannot alter recurrence settings for this item. If you need to change a recurrence pattern for this entry you will need to remove it and re-add from the parent entry. + + + This is the recurrence parent item. You cannot alter recurrence settings for this item. If you need to change a recurrence pattern you need to first remove then create a new recurrence entry. You can change any of the other details in the entry. + + + Reminder + + + Remove from this event + + + Remove Me + + + Repeat On + + + RSVP + + + second + + + Signup (RSVP) + + + third + + + Title of the calendar entry + + + Title of the calendar entry + + + Type Color + + + Type Name + + + Upcoming + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.es.resx b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.es.resx new file mode 100644 index 00000000..632964cb --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Calendar/Calendar.es.resx @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Todo el dia + + + Calendario + + + Detalle del evento + + + ADVERTENCIA: Esto eliminará permanentemente este tipo. ¿Estás seguro de que quieres eliminar el tipo? + + + Tipos de calendario + + + Días de la semana + + + Evento de eliminación + + + Eliminar evento y ocurrencias + + + Editar el tipo de calendario + + + Editar entrada de calendario + + + Este es el artículo principal de recurrencia. Si verifica la casilla de verificación "Aplicar a todos" que actualizará todos los elementos de recurrencia con los datos de este elemento. Si elimina este artículo, todos los artículos de recurrencia infantil también se eliminarán. + + + Evento de edición + + + Deja en blanco (vacío) para nunca caducar o terminar + + + Finalizará el + + + Entidades para notificar + + + primera + + + adelante + + + Última + + + Gestionar tipos + + + Administrar tipos de calendario + + + Nueva entrada de calendario + + + Nuevo tipo de calendario + + + Nueva entrada + + + Agregue una nueva entrada de calendario + + + Sin tipos personalizados + + + Ninguna + + + Sin nota + + + No hay elementos próximos + + + Este es el elemento de recurrencia del niño. No puede alterar la configuración de recurrencia para este elemento. Si necesita cambiar un patrón de recurrencia para esta entrada, deberá eliminarlo y volver a agregarlo desde la entrada principal. + + + Este es el artículo principal de recurrencia. No puede alterar la configuración de recurrencia para este elemento. Si necesita cambiar un patrón de recurrencia que primero debe eliminar, cree una nueva entrada de recurrencia. Puede cambiar cualquiera de los otros detalles en la entrada. + + + Recordatorio + + + Eliminar de este evento + + + Quitame + + + Repetir + + + RSVP + + + segunda + + + Registrarse (RSVP) + + + tercera + + + Título de la entrada del calendario + + + Título de la entrada del calendario + + + Tipo de color + + + Escribe un nombre + + + Próxima + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.cs b/Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.cs new file mode 100644 index 00000000..1b02b740 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.CustomStatuses +{ + public class CustomStatuses + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.en.resx b/Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.en.resx new file mode 100644 index 00000000..8512684d --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.en.resx @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Add Option (Button) + + + Button Background Color + + + Button Color + + + Button Preview + + + These are the buttons your users will see on mobile applications and the website. + + + Button Text + + + Button Text Color + + + Button Text + + + Calls + + + Calls and Stations + + + Changes to custom statues can take up to 30 minutes to fully propagate though the Resgrid network. Mobile app users are encouraged to manually resync (Settings->Advanced Settings->ReSync). + + + Custom Personnel Statuses (Actions) + + + Current Unit Statuses + + + Custom Personnel Staffing Levels + + + A description of the custom statuses. + + + Custom Statuses + + + Custom Statuses Set Name (i.e. 'My Custom Statuses') + + + Default Personnel Actions/Statuses + + + Default Personnel Staffing + + + WARNING: This will permanently delete this status set and your buttons will revert back to the defaults for this unit type. Are you sure you want to delete this status set? + + + Detail Type + + + Edit Custom State Detail + + + Edit Custom Statuses + + + Editing Set + + + Edit Personnel Statuses + + + Edit Staffing Levels + + + New Custom Statuses + + + New Option + + + No Detail + + + No Note + + + Note Type + + + Personnel Actions + + + Personnel Staffing + + + Personnel Statuses + + + Preview Button + + + Require GPS + + + Set Custom Staffing Levels + + + Set Custom Statuses + + + Staffing Levels + + + Stations + + + Text Color + + + Unit Status + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.es.resx b/Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.es.resx new file mode 100644 index 00000000..0f7db4d7 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/CustomStatuses/CustomStatuses.es.resx @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Añadir opción (botón) + + + Color de fondo del botón + + + Color del boton + + + Vista previa del botón + + + Estos son los botones que sus usuarios verán en aplicaciones móviles y el sitio web. + + + Botón de texto + + + Color de texto del botón + + + Botón de texto + + + Llamadas + + + Llamadas y estaciones + + + Los cambios en las estatuas personalizadas pueden tardar hasta 30 minutos en propagarse por completo a través de la red de regrid. Se alienta a los usuarios de aplicaciones móviles a recuperar manualmente (configuración-> Configuración avanzada-> resync). + + + Estados de personal personalizados (acciones) + + + Estado de la unidad actual + + + Niveles de personal personalizado de personal + + + Una descripción de los estados personalizados. + + + Estado personalizado + + + Estados personalizados Nombre del conjunto (es decir, 'mis estados personalizados') + + + Acciones/estados de personal predeterminados + + + Personal de personal predeterminado + + + Advertencia: esto eliminará permanentemente este conjunto de estado y sus botones volverán a los valores predeterminados para este tipo de unidad. ¿Estás seguro de que quieres eliminar este conjunto de estado? + + + Tipo de detalle + + + Editar detalles de estado personalizados + + + Editar estados personalizados + + + Set de edición + + + Editar estados de personal + + + Editar niveles de personal + + + Nuevos estados personalizados + + + Nueva opción + + + Sin detalle + + + Sin nota + + + Tipo de nota + + + Acciones de personal + + + Personal de personal + + + Estado de personal + + + Botón de vista previa + + + Requerir GPS + + + Establecer niveles de personal personalizados + + + Establecer estados personalizados + + + Niveles de empleo + + + Estaciones + + + Color de texto + + + Estado de la unidad + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.cs b/Core/Resgrid.Localization/Areas/User/Department/Department.cs new file mode 100644 index 00000000..9236acd7 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Department/Department.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Department +{ + public class Department + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.en.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.en.resx new file mode 100644 index 00000000..0c96d733 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Department/Department.en.resx @@ -0,0 +1,396 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Account Owner + + + Address + + + We were unable verify the address supplied. Check to ensure it's correct before you continue. + + + This is the address of your department; it could be the address of a single station or your district office. This address is used to help locate your district and area, and is used as a fallback for some operations (i.e. centering the big board). + + + Api & Rss Settings + + + API Settings + + + Call Import Settings + + + Call Sorting + + + City + + + City + + + Completed On + + + Country + + + Country + + + Default Map Center GPS Coordinates + + + Delete Invite + + + WARNING: This will permanently delete this invite and the link the user has in the email will result in an error. Are you sure you want to delete the invite? + + + Department Address + + + Department Name + + + Department Settings + + + Disable Auto-Available + + + Disable automatically setting personnel's status to Available after an hour + + + Dispatch Import Email + + + This is your ALL-CALL email address, all users in your department (who have call notifications enabled) will receive the call alert + + + Dispatch Settings + + + Dispatch Settings + + + Email Address + + + Email Addresses + + + Enter the email addresses separated by comma's (i.e. "test1@test.com, test2@test.com") + + + Email Type + + + Force Department Update + + + Resgrid caches some department data to increase performance. Some examples of data that is cached is staffing levels, department settings, personnel roles\groups\names. If you have issues where some department data is incorrect you might want to clear your departments cache. + + + Invites + + + Mapping Center Latitude + + + Latitude (Decimal Notation: i.e. 39.1517) + + + Mapping Center Longitude + + + Longitude (Decimal Notation: i.e. -119.4571) + + + Minutes to keep call open + + + The value above will keep email imported calls open for that long, then they will be closed. The Prune checkbox also needs to be checked for auto closing to occur. + + + Pending + + + Person Call Dispatch Status + + + Person Call Release Status + + + Personnel Sorting + + + Personnel Staffing Reset + + + If you need to reset all your personnel's staffing level every day, you can configure it here. An example would be resetting staffing to unavailable every day so that for red-flag days you are sure personnel that are marked available happened after the reset. + + + Personnel Status Reset + + + If you need to reset all your personnel's statuses every day, you can configure it here. An example would be resetting status to "Standing By" every day so that you know personnel status is reset. + + + Zip/Postal Code + + + Provision Rss\Atm Urls + + + Resgrid was unable provision the number you requested, please retry the operation with another number. If you cannot provision a number, please contact us and let us know. + + + Provision Failure + + + Prune (Auto Close) Calls + + + This will auto-close your automatically created calls (i.e. via an email, text or audio import process) + + + Regenerate RSS Urls + + + WARNING: Regenerating your Rss\Atom Urls will require all readers\consumers to update to the a new Url. Are you sure you want to regenerate? + + + Resend Invite + + + WARNING: This will resend this invite to the invite email address and reset the sent on date. Are you sure you want to resend this invite? + + + Reset Staffing Daily? + + + Reset Staffing Level To + + + Reset Status Daily? + + + Time to Reset Status + + + Reset Status To + + + RSS Active Call Feed + + + Resgrid allows your department’s active calls to be accessed via a RSS feed. This allows any feed reader (like a blog reader, or even Microsoft Outlook) to actively display and notify of new calls. For maximum compatible Resgrid will generate a unique URL for RSS 2.0 readers. This URL has no other protection (username\password, etc) so it’s important that it’s not publicly shared if you which not to have active calls data public. + + + Your department does not have RSS feed Urls provisioned. Click the button below to provision your RSS Urls. Only provision RSS\Atom Urls when your department needs it. + + + Successfully saved Dispatch Settings. + + + Send Invites + + + Sent On + + + Set Status for Shift Personnel On Dispatch + + + Shift Settings + + + State/Province + + + State/Province + + + Street + + + Street + + + Time to Reset Staffing + + + Time Zone + + + Unit Sorting + + + Use 24-Hour Time + + + Use Shift for Group Dispatch + + + Zip/Postal Code + + + Delete Department + + + Here you can request deletion of your department. Only the Account Owner can request the deletion of the department. All users and data associated with your department will be removed from the Resgrid system after the process is complete. This process is not reversable! At any time before the process is complete you can cancel the request and keep your data retained. Your department will be deleted 25 days after it’s been requested. + + + Request Deletion + + + Check the box below to confim your deletion request + + + I Understand and agree to the deletion process + + + Cancel Department Deletion + + + Delete Your Department + + + Request deletion of your department. This is a permanent and non-reversable operation that takes 25 days to complete and needs to run after hours. During the wait period you can cancel the request if you choose. + + + Request Department Deletion + + + Your department has a currently pending deletion request. If you want this request to proceed there is nothing you need to do after the timestamp below (during our nightly process) your department and all of it’s data will be removed. If you wish to cancel the request and not delete the department press the “Cancel Department Deletion” button below. Please note, any changes to admins and owners will cause the process to not complete, it’s recommended that you only use the system in a read only mode to download data you wish to save. + + + Enable Staffing Supress + + + Supress (Mute) For These Staffing Levels + + + Select the Personnel Staffing Levels below that will be prevented from receiving any notifications, dispatches, alerts, messages, or any other communication. Most commonly these are your "Off Duty" or "On Leave" staffing levels. + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.es.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.es.resx new file mode 100644 index 00000000..c9af2aa3 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Department/Department.es.resx @@ -0,0 +1,381 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Propietario de la cuenta + + + DIRECCIÓN + + + No pudimos verificar la dirección suministrada. Verifique para asegurarse de que sea correcto antes de continuar. + + + Esta es la dirección de su departamento; Podría ser la dirección de una sola estación o su oficina de distrito. Esta dirección se utiliza para ayudar a localizar su distrito y área, y se utiliza como alternativa para algunas operaciones (es decir, centrar la gran junta). + + + Configuración de API y RSS + + + Configuración de API + + + Configuración de importación de llamadas + + + Clasificación de llamadas + + + Ciudad + + + Ciudad + + + Completado en + + + País + + + País + + + Coordenadas GPS de Map Center predeterminadas + + + Eliminar invitación + + + ADVERTENCIA: Esto eliminará permanentemente esta invitación y el enlace que el usuario tiene en el correo electrónico dará como resultado un error. ¿Estás seguro de que quieres eliminar la invitación? + + + Dirección del departamento + + + Nombre de Departamento + + + Configuración del departamento + + + Desactivar automáticamente disponible + + + Desactivar automáticamente establecer el estado del personal en el que esté disponible después de una hora + + + Enviar correo electrónico de importación + + + Esta es su dirección de correo electrónico de atención, todos los usuarios de su departamento (que tienen notificaciones de llamadas habilitadas) recibirán la alerta de llamadas + + + Configuración de envío + + + Configuración de envío + + + Dirección de correo electrónico + + + Correos electrónicos + + + Ingrese las direcciones de correo electrónico separadas por la coma (es decir, "test1@test.com, test2@test.com") + + + Tipo de correo electrónico + + + Actualización del departamento de fuerza + + + REgrid almacena en caché algunos datos del departamento para aumentar el rendimiento. Algunos ejemplos de datos almacenados en caché son los niveles de personal, la configuración del departamento, los roles de personal \ Groups \ Names. Si tiene problemas en los que algunos datos del departamento son incorrectos, es posible que desee borrar el caché de los departamentos. + + + Invitaciones + + + Latitud del centro de mapeo + + + Latitud (notación decimal: es decir, 39.1517) + + + Longitud del centro de mapeo + + + Longitud (notación decimal: es decir, -119.4571) + + + Minutos para mantener la llamada abierta + + + El valor anterior mantendrá las llamadas importadas de correo electrónico abiertas por tanto tiempo, luego se cerrarán. La casilla de verificación de Prune también debe verificarse para que se produzca el cierre automático. + + + Pendiente + + + Estado de envío de llamadas de persona + + + Estado de liberación de llamadas de la persona + + + Clasificación de personal + + + Restablecimiento de personal de personal + + + Si necesita restablecer todo el nivel de personal de su personal todos los días, puede configurarlo aquí. Un ejemplo sería restablecer el personal para que no esté disponible todos los días, de modo que para los días de flag roja está seguro de que el personal está marcado disponible después del reinicio. + + + Restablecer el estado del personal + + + Si necesita restablecer todos los estados de su personal todos los días, puede configurarlo aquí. Un ejemplo sería restablecer el estado de "mantener" todos los días para que sepa que el estado del personal se restablece. + + + Código postal/postal + + + Provisión RSS \ ATM URLS + + + Resgrid no pudo provocar el número que solicitó, vuelva a intentar la operación con otro número. Si no puede aprovisionar un número, contáctenos y háganoslo saber. + + + Falla de la provisión + + + Prune (cierre automático) llamadas + + + Esto cerrará automáticamente sus llamadas creadas automáticamente (es decir, a través de un proceso de importación de correo electrónico, texto o audio) + + + Regenerar URL RSS + + + Advertencia: la regeneración de sus URL RSS \ Atom requerirá que todos los lectores \ consumidores actualicen a la nueva URL. ¿Estás seguro de que quieres regenerar? + + + Reenviar Invitación + + + ADVERTENCIA: Esto reenviará esta invitación a la dirección de correo electrónico de invitación y restablecerá la fecha enviada. ¿Estás seguro de que quieres reenviar esta invitación? + + + Reiniciar personal diariamente? + + + Restablecer el nivel de personal para + + + Restablecer el estado diariamente? + + + Tiempo para restablecer el estado + + + Restablecer el estado de + + + Feed de llamada activa RSS + + + Resgrid permite acceder a las llamadas activas de su departamento a través de una alimentación RSS. Esto permite que cualquier lector de feed (como un lector de blog, o incluso Microsoft Outlook) muestre y notifique activamente a nuevas llamadas. Para el regrid compatible máximo generará una URL única para los lectores RSS 2.0. Esta URL no tiene otra protección (nombre de usuario \ contraseña, etc.), por lo que es importante que no se comparta públicamente si no tiene públicos los datos de llamadas activas. + + + Su departamento no tiene URL de alimentación RSS aprovisionadas. Haga clic en el botón a continuación para provocar sus URL RSS. Solo elaborar URL RSS \ Atom cuando su departamento lo necesite. + + + Configurado con éxito la configuración de envío. + + + Enviar invitaciones + + + Enviado + + + Establecer el estado para el personal de turno en el envío + + + Configuración de cambio + + + Provincia del estado + + + Provincia del estado + + + Calle + + + Calle + + + Tiempo para restablecer el personal + + + Zona horaria + + + Clasificación de unidades + + + Use tiempo las 24 horas + + + Usar turno para el envío grupal + + + Código postal/postal + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Department/Department.resx b/Core/Resgrid.Localization/Areas/User/Department/Department.resx new file mode 100644 index 00000000..589ed1ba --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Department/Department.resx @@ -0,0 +1,23 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + + + + + + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.cs b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.cs new file mode 100644 index 00000000..aa22cfa7 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Dispatch +{ + public class Call + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx new file mode 100644 index 00000000..90fecf8e --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx @@ -0,0 +1,471 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Active Calls + + + Add a active call + + + Add Archived Call + + + Add Archived Call + + + Add an archived call + + + Added By + + + Add File + + + Enter attachment name + + + Add Image + + + Add Linked Call + + + Add Note + + + Enter note here... + + + Archived Calls + + + Attached Images + + + Call Documents + + + Resgrid Call Export + + + Call Form + + + Call Form + + + Call Identifier + + + CAD Id or Inbound Call System Id + + + Id + + + Call Links + + + Nature of the Call + + + A description of the nature of the call + + + Call Note Added By + + + Call Note Added On + + + Call Notes + + + Call Note Text + + + Call Number + + + Number + + + Calls Linked From (Parent Calls) + + + Calls Linked To (Children Calls) + + + Call Templates + + + Close Call + + + Close Call + + + Closed By + + + Closed On + + + Completed Notes + + + Notes about closing the call or the close reason + + + Completed Type + + + Create and Dispatch Call + + + Delete Call + + + Delete Call + + + Delete Reason + + + The reason for deleting the call + + + Dispatch Audio + + + Dispatched Personnel + + + Dispatched Units + + + Dispatch + + + Driving Directions from + + + This is an unauthenticated page for viewing this call only. You are not logged into Resgrid. Do not share this url. + + + File Name + + + File Type + + + Find Address + + + Find w3w Location + + + Flag Call Note + + + Flagged Reason + + + Reason for flagging + + + GPS + + + Incident Id + + + Main or Parent External Incident Id + + + Is Flagged + + + Learn More. + + + Linked Calls + + + Location + + + Full Address of the call (Street, City, State, Postal) + + + Logged By + + + Logged On + + + Map Page + + + Map Book Page + + + Name + + + Name of the Call + + + + + + New Call + + + No Attachments + + + No files attached to this Call + + + Not Closed + + + Note + + + Notes + + + Notes for the call + + + Not Supplied + + + No Type + + + Personnel Activity + + + Personnel + + + Personnel Events + + + Priority + + + Answer Protocol Questions + + + Protocols + + + Protocol Text + + + If you want to rebroadcast this call check this box. This will dispatch the call to all users selected, regardless if they were on the first dispatch or not. + + + ReDispatch + + + Reference Id + + + Partner or Reference Number + + + Regenerate Call Numbers + + + WARNING: This process will update ALL the Resgrid call numbers (i.e. 16-55) based on date. This process is not reversable. + + + Re-Open Call + + + Reporter Contact Info + + + Phone or Email address of the Reporter + + + Reporter Name + + + Name of the Person Reporting the call + + + Select Call + + + Note for Link (Optional) + + + Select Call Template + + + Select Call To Link + + + Send Notification + + + Set Template + + + State + + + Template + + + Timestamp + + + Type + + + Unit Activity + + + Unit Events + + + Units + + + Update Call + + + View Call + + + View Text + + + This is a what3words address. + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx new file mode 100644 index 00000000..3014e272 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx @@ -0,0 +1,471 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Llamadas activas + + + Agregar una llamada activa + + + Agregar llamada archivada + + + Agregar llamada archivada + + + Agregar una llamada archivada + + + Añadido por + + + Agregar archivo + + + Introduzca el nombre del archivo adjunto + + + Añadir imagen + + + Agregar llamada vinculada + + + Añadir la nota + + + Introduzca la nota aquí... + + + Llamadas archivadas + + + Imágenes adjuntas + + + Documentos de llamadas + + + Exportación de llamada de Resgrid + + + Formulario de llamada + + + Formulario de llamada + + + Identificador de llamada + + + ID de CAD o ID del sistema de llamadas entrantes + + + Identificación + + + Enlaces de llamadas + + + Naturaleza de la llamada + + + Una descripción de la naturaleza de la llamada. + + + Nota de llamada añadida por + + + Nota de llamada añadida el + + + Notas de llamadas + + + Texto de nota de llamada + + + Número de llamada + + + Número + + + Llamadas vinculadas desde (llamadas principales) + + + Llamadas vinculadas a (llamadas de niños) + + + Plantillas de llamadas + + + cerrar llamada + + + cerrar llamada + + + Cerrado por + + + Cerrado el + + + Notas Completadas + + + Notas sobre el cierre de la llamada o el motivo del cierre + + + Tipo completado + + + Crear y enviar llamadas + + + Eliminar llamada + + + Eliminar llamada + + + Borrar motivo + + + El motivo de la eliminación de la llamada. + + + Envío de audio + + + Personal Despachado + + + Unidades enviadas + + + Despacho + + + Indicaciones para llegar desde + + + Esta es una página no autenticada solo para ver esta llamada. No has iniciado sesión en Resgrid. No compartas esta URL. + + + Nombre del archivo + + + Tipo de archivo + + + Encuentra la dirección + + + Encuentra la ubicación de w3w + + + Marcar nota de llamada + + + Motivo marcado + + + Motivo de marcar + + + GPS + + + Identificación del incidente + + + ID de incidente externo principal o principal + + + está marcado + + + Aprende más. + + + Llamadas vinculadas + + + Ubicación + + + Dirección completa de la llamada (Calle, Ciudad, Estado, Postal) + + + Registrado por + + + Sesión iniciada + + + Página del mapa + + + Página del libro de mapas + + + Nombre + + + Nombre de la convocatoria + + + + + + Nueva llamada + + + Sin adjuntos + + + No hay archivos adjuntos a esta convocatoria + + + No se ha cerrado + + + Nota + + + notas + + + Notas para la llamada + + + No incluido + + + Sin tipo + + + Actividad del personal + + + Personal + + + Eventos de personal + + + Prioridad + + + Preguntas sobre el protocolo de respuesta + + + protocolos + + + Texto del protocolo + + + Si desea retransmitir esta llamada marque esta casilla. Esto enviará la llamada a todos los usuarios seleccionados, independientemente de si estaban en el primer envío o no. + + + Reenvío + + + Identificación de referencia + + + Socio o número de referencia + + + Regenerar números de llamada + + + ADVERTENCIA: Este proceso actualizará TODOS los números de llamada de Resgrid (es decir, 16-55) según la fecha. Este proceso no es reversible. + + + Volver a abrir la llamada + + + Información de contacto del reportero + + + Teléfono o dirección de correo electrónico del denunciante + + + Nombre del reportero + + + Nombre de la persona que reporta la llamada + + + Seleccione Llamar + + + Nota para enlace (opcional) + + + Seleccionar plantilla de llamada + + + Seleccione Llamar para vincular + + + Enviar notificación + + + Establecer plantilla + + + Estado + + + Plantilla + + + marca de tiempo + + + Tipo + + + Actividad de la unidad + + + Eventos de unidad + + + Unidades + + + Actualizar llamada + + + Ver llamada + + + Ver texto + + + Esta es una dirección de what3words. + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.cs b/Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.cs new file mode 100644 index 00000000..8cd2fdf9 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Dispatch +{ + public class Dashboard + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.en.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.en.resx new file mode 100644 index 00000000..fa91f9ef --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.en.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Active Calls + + + Archived Calls + + + New Call + + + Add a new call + + + Unit Statuses + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.es.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.es.resx new file mode 100644 index 00000000..72f18230 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Dashboard.es.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Llamadas activas + + + Llamadas archivadas + + + Nueva llamada + + + Agregue una nueva llamada + + + Estado de la unidad + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Home/EditProfile.cs b/Core/Resgrid.Localization/Areas/User/Home/EditProfile.cs new file mode 100644 index 00000000..5db63db6 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Home/EditProfile.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Home +{ + public class EditProfile + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Home/EditProfile.en.resx b/Core/Resgrid.Localization/Areas/User/Home/EditProfile.en.resx new file mode 100644 index 00000000..3b186d54 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Home/EditProfile.en.resx @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Account Information + + + Address + + + Physical Street Address + + + Email + + + Call Options + + + Push + + + Text + + + Certifications + + + Change Users Password + + + City + + + Physical City + + + If your changing your password you need to re-enter your NEW password here + + + Confirm Password + + + Country + + + If your changing your password you need enter your current password (not the new one) here + + + Current Password + + + Delete Your Account + + + Delete your own Resgrid account in all departments you are a member of + + + Department Admin + + + Department Settings + + + Disable User + + + Edit + + + Email Addresses must be unique + + + Email Address + + + Email Address + + + There were errors in the profile below and it could not be saved. Please correct the errors below and try saving again. + + + First Name + + + First Name + + + Group + + + Hide User + + + Home Number + + + Home Phone Number + + + ID Number + + + Custom Identication Number + + + Is Group Admin? + + + Note: Multi-lingual support in Resgrid is not fully supported. Some parts of the website will still be in English and we are working to get everything translated. Our support services (Helpdesk, Github, Discord, etc) and documentation are only available in English. + + + Last Name + + + Last Name + + + Address + + + Mailing Street Address + + + City + + + Mailing City + + + Country + + + Postal Code + + + Mailing Zip\Postal Code + + + State + + + Mailing State\Province + + + Email + + + Message Options + + + Push + + + Text + + + Mobile Carrier + + + + + + Mobile Number + + + Mobile Phone Number + + + Leave blank if you don't want to change your password + + + New Password + + + Don't Receive Newsletters + + + Leave blank if you don't want to change your UserName (i.e. keep your current UserName that you log in with) + + + New Username + + + Normal User + + + Email + + + Notification Options + + + Other Notification Options + + + Push + + + Physical Address + + + Postal Code + + + Physical Zip\Postal Code + + + Profile + + + Report Delivery + + + Roles + + + Same as Physical? + + + picture changes may take up to 24 hours + + + click here to set a picture + + + Staffing Schedules + + + State + + + Physical State\Province + + + Call Home + + + Call Mobile + + + Enabled + + + Telephone (Voice) Options + + + Wrong Plan + + + Your department's current plan doesn't support voice Telephone Alerting. Upgrade your plan to Ultimate or higher to get this feature. + + + Time Zone + + + Edit Profile + + + User Type + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Home/EditProfile.es.resx b/Core/Resgrid.Localization/Areas/User/Home/EditProfile.es.resx new file mode 100644 index 00000000..2edfd98a --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Home/EditProfile.es.resx @@ -0,0 +1,372 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Información de la cuenta + + + DIRECCIÓN + + + Dirección de la calle física + + + Correo electrónico + + + Opciones de llamada + + + Empujar + + + Texto + + + Certificaciones + + + Cambiar la contraseña de los usuarios + + + Ciudad + + + Ciudad física + + + Si está cambiando su contraseña, necesita volver a ingresar su nueva contraseña aquí + + + confirmar Contraseña + + + País + + + Si está cambiando su contraseña, necesita ingresar su contraseña actual (no la nueva) aquí + + + Contraseña actual + + + Administrador del departamento + + + Configuración del departamento + + + Desactivar el usuario + + + Editar + + + Las direcciones de correo electrónico deben ser únicas + + + Dirección de correo electrónico + + + Dirección de correo electrónico + + + Hubo errores en el perfil a continuación y no se pudo guardar. Corrija los errores a continuación e intente guardar nuevamente. + + + Nombre de pila + + + Nombre de pila + + + Grupo + + + Ocultar el usuario + + + Número de casa + + + Número de teléfono de casa + + + Número de identificación + + + Número de identificación personalizado + + + ¿Es el administrador del grupo? + + + Nota: El soporte multilingüe en REGRID no es totalmente compatible. Algunas partes del sitio web seguirán en inglés y estamos trabajando para traducir todo. Nuestros servicios de soporte (HelpDesk, GitHub, Discord, etc.) y la documentación solo están disponibles en inglés. + + + Apellido + + + Apellido + + + DIRECCIÓN + + + Dirección de la calle del correo + + + Ciudad + + + Ciudad de correo + + + País + + + Código Postal + + + Correo postal \ código postal + + + Estado + + + Estado de correo \ Provincia + + + Correo electrónico + + + Opciones de mensajes + + + Empujar + + + Texto + + + Operador de telefonía móvil + + + + + + Número de teléfono móvil + + + Número de teléfono móvil + + + Deje en blanco si no desea cambiar su contraseña + + + Nueva contraseña + + + No Recibir Boletines + + + Deje en blanco si no desea cambiar su nombre de usuario (es decir, mantenga su nombre de usuario actual con el que inicie sesión) + + + Nuevo nombre de usuario + + + Usuario normal + + + Correo electrónico + + + Opciones de notificación + + + Otras opciones de notificación + + + Empujar + + + Dirección física + + + Código Postal + + + Zip físico \ Código postal + + + Perfil + + + Entrega de informes + + + Roles + + + ¿Igual que el físico? + + + Los cambios en las imágenes pueden tomar hasta 24 horas + + + Haga clic aquí para configurar una imagen + + + Horarios de personal + + + Estado + + + Estado físico \ Provincia + + + Llama a casa + + + Llamar móvil + + + Activada + + + Opciones de teléfono (voz) + + + Plan incorrecto + + + El plan actual de su departamento no es compatible con la alertas de teléfono de voz. Actualice su plan a Ultimate o superior para obtener esta función. + + + Zona horaria + + + Editar perfil + + + Tipo de usuario + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.cs b/Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.cs new file mode 100644 index 00000000..88f64bb0 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Home +{ + public class HomeDashboard + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.en.resx b/Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.en.resx new file mode 100644 index 00000000..1f4fa34d --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.en.resx @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Action Note (Optional) + + + Actions + + + Are you sure you want to reset all personnel status to Available? + + + Collapse Groups (Once) + + + CONFIRM: Are you sure you want to reset all the users group to the Standing By state? Group: + + + Confirm Status Reset + + + Department Code: + + + Department Id: + + + Department Info + + + Reset all to Available + + + Reset group to Standing By + + + Reset Status + + + Set Current Action + + + Set Staffing Level + + + Staffing Level + + + Set Staffing Level + + + Text Number: + + + Toggle Collapse Groups (Saves) + + diff --git a/Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.es.resx b/Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.es.resx new file mode 100644 index 00000000..9c5d2cc2 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Home/HomeDashboard.es.resx @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Nota de acción (opcional) + + + Comportamiento + + + ¿Estás seguro de que quieres restablecer todo el estado del personal disponible? + + + Grupos de colapso (una vez) + + + Confirmar: ¿Está seguro de que desea restablecer todo el grupo de usuarios al estado de pie por estado? Grupo: + + + Restablecer el estado de confirmación + + + Código del departamento: + + + ID de departamento: + + + Información del departamento + + + Restablecer todo a disponible + + + Restablecer el grupo para estar de pie por + + + Reestablezca su estatus + + + Establecer acción actual + + + Establecer el nivel de personal + + + Nivel de personal + + + Establecer el nivel de personal + + + Número de texto: + + + Grupos de colapso de al revés (ahorros) + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Logs/Logs.cs b/Core/Resgrid.Localization/Areas/User/Logs/Logs.cs new file mode 100644 index 00000000..148375b9 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Logs/Logs.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Logs +{ + public class Logs + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Logs/Logs.en.resx b/Core/Resgrid.Localization/Areas/User/Logs/Logs.en.resx new file mode 100644 index 00000000..ce571174 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Logs/Logs.en.resx @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Add Unit to Log + + + Attachments + + + Body Location + + + Body Location is required. + + + Call Name + + + Call Priority + + + Case # + + + Cause + + + Condition + + + Coroner + + + Date + + + WARNING: This will permanently delete this log. Are you sure you want to delete the log? + + + Destination Location + + + Dispatched + + + Investigator + + + Location + + + Log Information + + + Logs for Year + + + Logs + + + Meeting + + + Meeting end time is required. + + + Meeting location is required. + + + Meeting start time is required. + + + Meeting Type + + + Meeting type is required. + + + Narrative + + + New Call + + + New Log + + + New Log + + + Below you can add a new log to the system (i.e. run report, station work, training, event, etc). Logs an be used to detail information about call runs, meetings and trainings. Fields in blue italics are required. + + + Other Attendees + + + Others Having Contact with Body + + + Personnel + + + personnel required + + + Presiding Person(s) + + + Pronounced Deceased By + + + Pronounced Deceased By is required. + + + Date is required. + + + Select Call + + + Senior (OIC) + + + Training end time is required. + + + Training start time is required. + + + Unit Personnel + + + Units + + + Units and Personnel + + + Work end time is required. + + + Work start time is required. + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Logs/Logs.es.resx b/Core/Resgrid.Localization/Areas/User/Logs/Logs.es.resx new file mode 100644 index 00000000..eb637cf4 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Logs/Logs.es.resx @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Agregar unidad al registro + + + Archivos adjuntos + + + Ubicación del cuerpo + + + Se requiere la ubicación del cuerpo. + + + Nombre de llamada + + + Prioridad de llamadas + + + Caso # + + + Causa + + + Condición + + + Juez de instrucción + + + Fecha + + + ADVERTENCIA: Esto eliminará permanentemente este registro. ¿Estás seguro de que quieres eliminar el registro? + + + Ubicación de destino + + + Enviada + + + Investigadora + + + Ubicación + + + Información de registro + + + Registros para el año + + + Registro + + + Reunión + + + Se requiere tiempo de finalización de la reunión. + + + Se requiere la ubicación de la reunión. + + + Se requiere la hora de inicio de la reunión. + + + Tipo de reunión + + + Se requiere el tipo de reunión. + + + Narrativa + + + Nueva llamada + + + Nuevo registro + + + Nuevo registro + + + A continuación puede agregar un nuevo registro al sistema (es decir, el informe de ejecución, el trabajo de la estación, la capacitación, el evento, etc.). Los registros se utilizarán para detallar la información sobre las ejecuciones de llamadas, reuniones y entrenamientos. Se requieren campos en cursiva azul. + + + Otros asistentes + + + Otros que tienen contacto con el cuerpo + + + Personal + + + requerido el personal + + + Personas presidiadas + + + Pronunciado fallecido por + + + Se requiere pronunciado fallecido por. + + + Se requiere fecha. + + + Seleccionar llamada + + + Senior (OIC) + + + Se requiere tiempo de finalización de capacitación. + + + Se requiere la hora de inicio de la capacitación. + + + Personal de la unidad + + + Unidades + + + Unidades y personal + + + Se requiere tiempo de finalización del trabajo. + + + Se requiere la hora de inicio del trabajo. + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Messages/Messages.cs b/Core/Resgrid.Localization/Areas/User/Messages/Messages.cs new file mode 100644 index 00000000..90c84825 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Messages/Messages.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Messages +{ + public class Messages + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Messages/Messages.en.resx b/Core/Resgrid.Localization/Areas/User/Messages/Messages.en.resx new file mode 100644 index 00000000..2adcaec5 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Messages/Messages.en.resx @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Attachments + + + Callback + + + Compose Message + + + WARNING: This will permanently delete this message. Are you sure you want to delete the message + + + Delete Selected + + + Exclude Shifts + + + Expires + + + Expires + + + Folders + + + From + + + Inbox + + + Mark Selected as Read + + + Message + + + Messages Inbox + + + Messages Outbox + + + Never + + + No Group + + + Supply a note in the text box above before pressing a response button below if you want to send a note with your reponse. + + + Outbox + + + Poll + + + Read On + + + Recipients + + + Recipients + + + Response + + + Send to all Personnel + + + Send to Personnel only in selected Role(s)/Group(s) + + + Sent Messages + + + Sent Messages + + + Subject + + + The subject/title of the message + + + To + + + Type + + + Unread + + + View Message + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Messages/Messages.es.resx b/Core/Resgrid.Localization/Areas/User/Messages/Messages.es.resx new file mode 100644 index 00000000..249ffafa --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Messages/Messages.es.resx @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Archivos adjuntos + + + Llamar de vuelta + + + Crear Mensaje + + + ADVERTENCIA: Esto eliminará permanentemente este mensaje. ¿Estás seguro de que quieres eliminar el mensaje? + + + Eliminar seleccionado + + + Excluir cambios + + + Expirar + + + Expirar + + + Carpetas + + + De + + + Bandeja de entrada + + + Mark seleccionado como leído + + + Mensaje + + + Bandeja de entrada de mensajes + + + Bandeja de salida de mensajes + + + Nunca + + + No hay grupo + + + Suministre una nota en el cuadro de texto anterior antes de presionar un botón de respuesta a continuación si desea enviar una nota con su respuesta. + + + Bandeja de salida + + + Encuesta + + + Seguir leyendo + + + Receptores + + + Receptores + + + Respuesta + + + Enviar a todo el personal + + + Enviar al personal solo en roles seleccionados/grupos + + + Mensajes enviados + + + Mensajes enviados + + + Sujeto + + + El sujeto/título del mensaje + + + A + + + Tipo + + + No leído + + + Ver mensaje + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Notes/Note.cs b/Core/Resgrid.Localization/Areas/User/Notes/Note.cs new file mode 100644 index 00000000..c11e35aa --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Notes/Note.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Notes +{ + public class Note + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Notes/Note.en.resx b/Core/Resgrid.Localization/Areas/User/Notes/Note.en.resx new file mode 100644 index 00000000..de5c8962 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Notes/Note.en.resx @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Body + + + The text content for the note + + + Category + + + Select an existing Category from the dropdown or type in the box to create a Category. + + + Delete Note + + + WARNING: This will permanently delete this note. Are you sure you want to delete the note? + + + Edit Note + + + New Note + + + Add a new note + + + Title + + + Title of the Note + + + Viewable By + + + Viewable by Admins Only + + + Viewable by Everyone + + + View Note + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Notes/Note.es.resx b/Core/Resgrid.Localization/Areas/User/Notes/Note.es.resx new file mode 100644 index 00000000..9cfa436f --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Notes/Note.es.resx @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cuerpo + + + El contenido del texto de la nota. + + + Categoría + + + Seleccione una categoría existente del menú desplegable o escriba en el cuadro para crear una categoría. + + + Borrar nota + + + ADVERTENCIA: Esto eliminará permanentemente esta nota. ¿Está seguro de que desea eliminar la nota? + + + Editar nota + + + Nueva nota + + + Agregar una nueva nota + + + Título + + + Título de la nota + + + Visible por + + + Visible solo por administradores + + + Visible por todos + + + Ver nota + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Personnel/Person.cs b/Core/Resgrid.Localization/Areas/User/Personnel/Person.cs new file mode 100644 index 00000000..9a07bdd1 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Personnel/Person.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Personnel +{ + public class Person + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Personnel/Person.en.resx b/Core/Resgrid.Localization/Areas/User/Personnel/Person.en.resx new file mode 100644 index 00000000..4f05a61d --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Personnel/Person.en.resx @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Account Information + + + Add a Person + + + Add a single person + + + Existing User Added + + + Add Person + + + Add Rank + + + Add Role + + + Call Options + + + Change Password + + + Click the checkbox below to delete this user. + + + Confirm Delete? + + + Confirm the users password (must match the one above) + + + Confirm Password + + + Contact Details + + + Are you sure you want to delete this user? + + + If you choose to delete this user the information below will be permanently deleted. The recommended procedure is to disable the user if they leave the department or organization. Only delete the user after enough time has gone by, the account was a mistake or never used. + + + Delete Person + + + Edit Role + + + Email + + + Email Addresses must be unique + + + Email Address + + + Email Address (must be unique) + + + This user + + + , based on email address, already had a Resgrid account in another department. The user account has now been added <b>with their previous profile settings (name, phone numbers, preferences, etc)</b> to the department. Because the user is in multiple departments they need activate this department to see it's information from their "View Your Departments" option under their profile dropdown (under the profile picture in the upper left hand corner). + + + First Name + + + First Name + + + Group + + + Home + + + ID Number + + + Custom Identication Number + + + Is Group Admin? + + + Last Name + + + Last Name + + + Manage Invites + + + Invite multiple people + + + Manage Roles + + + Message Options + + + Mobile + + + Mobile Carrier + + + Mobile Number + + + Mobile Phone Number + + + Passwords must be 8 characters or longer and include a digit (number), an uppercase and lowercase letter + + + New Password + + + No Personnel In Department + + + No Personnel in this Group + + + Notification Options + + + Notify User? + + + Uncheck if you don't want Resgrid to notify the user of this account creation. + + + No UnGrouped Personnel + + + Personnel + + + Phone Number(s) + + + Push + + + WARNING: This will permanently delete this rank. Are you sure you want to delete the rank + + + Personnel Ranks + + + Reactivate Person + + + This user + + + based on email address, already had an account in the + + + department but was deleted previously. The user account has now be un-deleted and reactivated inside this department and will appear <b>with their previous profile settings (name, phone numbers, preferences, etc)</b> in the department. + + + WARNING: This will permanently delete this role. Are you sure you want to delete the role + + + A description of the role + + + Name of the Role + + + Personnel Roles + + + Roles + + + Set Person Staffing + + + Set Person Status + + + Set Staffing + + + Set Staffing for Selected Personnel + + + Set Status + + + Set Status for Selected Personnel + + + Special Note + + + Underlying user data is retained in the system so that logs, reports and history is properly retained for the department. It is recommended that all PII/PHI in the user account is altered to be removed, i.e. phone numbers, addresses, etc before you delete the user. + + + Staffing + + + State + + + Text + + + You may incur additional changes for SMS/Text depending on your mobile plan. + + + User + + + User Details + + + The User Name must be unique + + + Username + + + Users in Rank + + + Users In Role + + + View Person + + + View Role + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Personnel/Person.es.resx b/Core/Resgrid.Localization/Areas/User/Personnel/Person.es.resx new file mode 100644 index 00000000..b898f13c --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Personnel/Person.es.resx @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Información de la cuenta + + + Agregar una persona + + + Añadir una sola persona + + + Usuario existente añadido + + + Agregar persona + + + Agregar rango + + + Agregar rol + + + Opciones de llamada + + + Cambiar la contraseña + + + Haga clic en la casilla de verificación a continuación para eliminar este usuario. + + + ¿Confirmar eliminación? + + + Confirme la contraseña de los usuarios (debe coincidir con la anterior) + + + confirmar Contraseña + + + Detalles de contacto + + + ¿Está seguro de que desea eliminar este usuario? + + + Si elige eliminar a este usuario, la información a continuación se eliminará de forma permanente. El procedimiento recomendado es deshabilitar al usuario si deja el departamento u organización. Solo elimine el usuario después de que haya pasado suficiente tiempo, la cuenta fue un error o nunca se usó. + + + Eliminar persona + + + Editar rol + + + Correo electrónico + + + Las direcciones de correo electrónico deben ser únicas + + + Dirección de correo electrónico + + + Dirección de correo electrónico (debe ser única) + + + este usuario + + + , según la dirección de correo electrónico, ya tenía una cuenta de Resgrid en otro departamento. La cuenta de usuario ahora se ha agregado <b>con su configuración de perfil anterior (nombre, números de teléfono, preferencias, etc.)</b> al departamento. Debido a que el usuario está en varios departamentos, necesita activar este departamento para ver su información desde su opción "Ver sus departamentos" en el menú desplegable de su perfil (debajo de la imagen de perfil en la esquina superior izquierda). + + + Nombre de pila + + + Nombre de pila + + + Grupo + + + Hogar + + + Número de identificación + + + Número de identificación personalizado + + + ¿Es administrador del grupo? + + + Apellido + + + Apellido + + + Administrar invitaciones + + + Invitar a varias personas + + + Administrar funciones + + + Opciones de mensaje + + + Móvil + + + Operador de telefonía móvil + + + Número de teléfono móvil + + + Número de teléfono móvil + + + Las contraseñas deben tener 8 caracteres o más e incluir un dígito (número), una letra mayúscula y minúscula + + + Nueva contraseña + + + Sin personal en el departamento + + + No hay personal en este grupo + + + Opciones de notificación + + + ¿Notificar al usuario? + + + Desmarque si no desea que Resgrid notifique al usuario sobre la creación de esta cuenta. + + + Sin Personal Desagrupado + + + Personal + + + Números de teléfono) + + + Empujar + + + ADVERTENCIA: Esto eliminará permanentemente este rango. ¿Está seguro de que desea eliminar el rango? + + + Rangos de personal + + + Reactivar persona + + + este usuario + + + basado en la dirección de correo electrónico, ya tenía una cuenta en el + + + departamento pero fue borrado previamente. La cuenta de usuario ahora se ha recuperado y reactivado dentro de este departamento y aparecerá <b>con su configuración de perfil anterior (nombre, números de teléfono, preferencias, etc.)</b> en el departamento. + + + ADVERTENCIA: Esto eliminará permanentemente este rol. ¿Estás seguro de que quieres eliminar el rol? + + + Una descripción del rol. + + + Nombre del Rol + + + Funciones del personal + + + roles + + + Establecer personal de persona + + + Establecer estado de persona + + + Establecer dotación de personal + + + Establecer dotación de personal para el personal seleccionado + + + Establecer estado + + + Establecer estado para el personal seleccionado + + + Nota especial + + + Los datos de usuario subyacentes se conservan en el sistema para que los registros, los informes y el historial se conserven correctamente para el departamento. Se recomienda modificar toda la PII/PHI de la cuenta de usuario para eliminarla, es decir, números de teléfono, direcciones, etc., antes de eliminar al usuario. + + + dotación de personal + + + Estado + + + Texto + + + Puede incurrir en cambios adicionales para SMS/Texto dependiendo de su plan móvil. + + + Usuario + + + Detalles de usuario + + + El nombre de usuario debe ser único + + + Nombre de usuario + + + Usuarios en rango + + + Usuarios en Rol + + + Ver persona + + + Ver rol + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.cs b/Core/Resgrid.Localization/Areas/User/Profile/Profile.cs new file mode 100644 index 00000000..13515600 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Profile +{ + public class Profile + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.en.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.en.resx new file mode 100644 index 00000000..387743b4 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.en.resx @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Add Certification + + + Add Certification + + + Enter the information below to edit a certification in your profile. Example certifications are EMT certs, CPR/BLS, Hazmat, etc. Tracking certifications can be useful to ensure they are updated/renewed before they expire and the department knows who's certified for what. Fields in <span class="required">blue italics</span> are required. + + + Add Scheduled Report + + + Add Staffing Schedule + + + WARNING: This will permanently delete this certification. Are you sure you want to delete the certification? + + + ID # \ Cert # + + + ID # \ Cert # + + + Certification Name + + + Certifications + + + Change Password + + + Change Password For User + + + Here you can manually change the password for a user in the system. Enter in the new password and confirm it to change it. Passwords must be at least 6 characters in length. Note that Resgrid will not notify the user of this password change. Fields in <span class="required">blue italics</span> are required. + + + Change Staffing To + + + Confirm Password + + + Confirm the Password for this User + + + Days of the Week + + + Department Code + + + Department Id + + + Enter the information below to edit a certification in your profile. Example certifications are EMT certs, CPR/BLS, Hazmat, etc. Tracking certifications can be useful to ensure they are updated/renewed before they expire and the department knows who's certified for what. Fields in <span class="required">blue italics</span> are required. + + + Edit Certification + + + Edit Profile + + + Edit Scheduled Report + + + Edit Staffing Schedule + + + Email User? + + + Check this box if you want the user Emailed with their UserName and new Password. + + + Expires On + + + Expires On + + + Issued By + + + Issuing Authority name + + + Issued On + + + Join Department + + + Join Department + + + You need to supply the current Department Id and Code for the Resgrid Department you wish to join. + + + New Password + + + New Password for this User + + + Passwords must be 8 characters or longer and include a digit (number), an uppercase and lowercase letter + + + New Scheduled Report + + + New Staffing Schedule + + + WARNING: This will remove you from this department. Are you sure you sure? + + + Remove Yourself From Department + + + Report Delivery + + + Report Delivery Schedules + + + Report To Run + + + Schedules + + + Set As Active + + + WARNING: This will change your current active department, you can only be logged into one department at a time, are you sure? + + + Set As Default + + + WARNING: This will change your default department, if you want to view this department you will need to activate it afterwards. + + + Specific Date + + + Specific DateTime + + + Staffing Schedule + + + Staffing Schedule + + + Time To Run + + + Valid Area + + + Area certification in valid in + + + Your Departments + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Profile/Profile.es.resx b/Core/Resgrid.Localization/Areas/User/Profile/Profile.es.resx new file mode 100644 index 00000000..568139df --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Profile/Profile.es.resx @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Agregar certificación + + + Agregar certificación + + + Ingrese la información a continuación para editar una certificación en su perfil. Las certificaciones de ejemplo son CERTS EMT, CPR/BLS, HAZMAT, etc. Las certificaciones de seguimiento pueden ser útiles para garantizar que se actualicen/renovan antes de que expiren y el departamento sabe quién está certificado para qué. Se requieren campos en <span class = "requerido"> Cursos de azul </span>. + + + Agregar informe programado + + + Agregar horario de personal + + + ADVERTENCIA: Esto eliminará permanentemente esta certificación. ¿Estás seguro de que quieres eliminar la certificación? + + + ID # \ cert # + + + ID # \ cert # + + + Nombre de certificacion + + + Certificaciones + + + Cambiar la contraseña + + + Cambiar contraseña para el usuario + + + Aquí puede cambiar manualmente la contraseña para un usuario en el sistema. Ingrese en la nueva contraseña y confirme para cambiarla. Las contraseñas deben tener al menos 6 caracteres de longitud. Tenga en cuenta que Resgrid no notificará al usuario sobre este cambio de contraseña. Se requieren campos en <span class = "requerido"> Cursos de azul </span>. + + + Cambiar el personal a + + + confirmar Contraseña + + + Confirme la contraseña de este usuario + + + Días de la semana + + + código del departamento + + + ID de departamento + + + Ingrese la información a continuación para editar una certificación en su perfil. Las certificaciones de ejemplo son CERTS EMT, CPR/BLS, HAZMAT, etc. Las certificaciones de seguimiento pueden ser útiles para garantizar que se actualicen/renovan antes de que expiren y el departamento sabe quién está certificado para qué. Se requieren campos en <span class = "requerido"> Cursos de azul </span>. + + + Editar certificación + + + Editar perfil + + + Editar informe programado + + + Editar calendario de personal + + + ¿Usuario de correo electrónico? + + + Marque esta casilla si desea que el usuario envíe un correo electrónico con su nombre de usuario y su nueva contraseña. + + + Expira el + + + Expira el + + + Expedido por + + + Nombre de la autoridad emisora + + + Emitido por + + + Se une al departamento + + + Se une al departamento + + + Debe proporcionar la identificación y código del departamento actual para el departamento de regrid que desea unirse. + + + Nueva contraseña + + + Nueva contraseña para este usuario + + + Las contraseñas deben ser de 8 caracteres o más e incluir un dígito (número), una letra mayúscula y minúscula + + + Nuevo informe programado + + + Nuevo horario de personal + + + ADVERTENCIA: Esto lo eliminará de este departamento. ¿Estás seguro de que estás seguro? + + + Quítate del departamento + + + Entrega de informes + + + Horario de entrega de informes + + + Informe para ejecutar + + + Horario + + + Establecido como activo + + + ADVERTENCIA: Esto cambiará su departamento activo actual, solo puede iniciar sesión en un departamento a la vez, ¿está seguro? + + + Establecer por defecto + + + ADVERTENCIA: Esto cambiará su departamento predeterminado, si desea ver este departamento, deberá activarlo después. + + + Fecha específica + + + DateTime específico + + + Horario de personal + + + Horario de personal + + + Hora de correr + + + Área válida + + + Certificación de área en válido en + + + Tus departamentos + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.cs b/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.cs new file mode 100644 index 00000000..e92b0b3a --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.cs @@ -0,0 +1,7 @@ +namespace Resgrid.Localization.Areas.User.Subscription +{ + public class Subscription + { + + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.en.resx b/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.en.resx new file mode 100644 index 00000000..4fc72fd3 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.en.resx @@ -0,0 +1,471 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Billing Update Success + + + Thank you for updating your billing information. Your new card has not been charged right now but will be used when the subscription renews at it's normal time. + + + Buy Addon + + + Your are buying the following addon + + + Your above addon purchase will be added to your existing subscription and prorated for the time remaining on your current billing frequency. Your current card on file will be charged please make sure it's up to date to avoid a service interruption. + + + Warning + + + You have a canceled addon of this type already with time still remaining. To avoid double billing please wait till this time to add this addon again. If you don't wish to wait you can add now and some porition of the remaining time may be prorated. Your remaining time ends on + + + Buy Now + + + Buy Now + + + You are buying the following plan + + + for + + + year + + + month + + + Your next payment will be on + + + You are switching from a yearly plan to a monthly plan and retain the credit from your yearly plan. Once that credit is used up you will be billed on a normal monthly schedule. + + + Your estimated upgrade total is + + + Estimated cost, actual cost may be slightly different. + + + Credit Card Billing Address + + + Billing street address of credit card + + + Must match the street address where credit card billing statements are sent + + + Billing zip/postal code + + + Billing country + + + Payment information is secured with SSL encryption. <b>Resgrid DOES NOT store any payment info.</b> + + + First name on card + + + Last name on card + + + Full credit card number + + + CVV/CVC number + + + 3 or 4 digit number on the card + + + more info + + + Credit Card Expiration + + + Cancel Subscription + + + We are sad to see you cancel your subscription. Please fill out the form below to complete your subscription cancel request. The reason field is optional, please make sure you check the confirm box and press ok to the browser confirmation prompt when it appears. + + + Your department and all your data will not be removed, just your subscription renewal will be canceled. For the remaining time on your subscription you will still have access to existing features part of that plan. + + + Plan + + + Current Plan + + + Bought On + + + Confirm + + + Reason + + + Back To Subscriptions + + + Cancel Subscription + + + WARNING: This will cancel your active subscription. Are you sure? + + + Cancel Failure + + + We were unable to cancel your subscription. This can be because you don't have an active subscription (i.e. your on the free plan) or due to a temporary issue with the payment processor. Please try again in a few minutes. If you keep getting this issue contact us to help resolve it. + + + Subscription Canceled + + + Your subscription has been canceled. It may take a few minutes for the system to reflect this but your card will now longer be charged. We will keep your data safe in case you want to come back, or use the Free Resgrid plan. + + + Subscription Plan & Billing + + + View Payment History + + + Change Billing Info + + + Canceled + + + Active + + + Plan Expires + + + Plan Renews + + + Personnel + + + Groups + + + Units + + + Warning + + + Standard + + + Premium + + + Professional + + + Ultimate + + + Enterprise + + + Unified + + + Inbound Texting + + + Telephone Alerting + + + Price + + + per year + + + per month + + + Amount + + + Your are buying the following plan + + + Buy Monthly + + + Larger plans, private hosting, priority support, open source support and implementation services are also available. + + + for more info. + + + Buy Yearly + + + If you wish to make the change in your subscription press the "Change Plan" button below. Our payment provider will calculate the proration of your current plan's time and determine the balance due and charge your card on file (if necessary). You will receive a email with the details once the change has been processed. + + + Contact us + + + Cost + + + Customer + + + Department + + + Description + + + End Date + + + Invoice + + + Invoice Date + + + Method + + + Thank you for your purchase. Your payment is now complete your addon or plan upgrade should be available soon. You may have to log out and log back in to see the changes. + + + Payment Complete + + + We're sorry but there was an issue trying to process your payment. Press the back button on your browser and verify your payment information (i.e. zip/postal code, CCV number, expiration). + + + Payment Failed + + + Payment History + + + Due to higher then expected volume your payment is currently in a queue. You can leave this page and utilize Resgrid as normal. Once we have processed the payment your account will be upgraded and you will receive an email from us. If you don't receive an email from us within 30 minutes please contact us at team@resgrid.com with your issue. + + + Payment Pending + + + Thank you for your purchase. Your payment is currently being processed. Please wait until our payment processor finishes and your department will be upgraded. + + + Payment Processing + + + PayPal + + + Period + + + Processor + + + Purchase Date + + + Start Date + + + Stripe + + + Thank you for updating your billing information. Your new card has not been charged right now but will be used when the subscription renew's at it's normal time. + + + Billing Update Success + + + Thank you for your purchase. Your payment is currently being processed. Please wait until Stripe, our payment processor, finishes and your department will be upgraded. + + + Payment Processing + + + System + + + Thank You! + + + Total + + + Transaction + + + Transaction Id + + + Thank you for your interest in buying a plan for the Resgrid service. We already have an upcoming plan for your department and we only allow 1 future plan per customer. If you want to buy multiple years feel free to contact us and we'd be glad to assist you. + + + Unable to Purchase + + + Upcoming Plan + + + Update Billing Info + + + Change Plan + + + View Invoice + + + You are currently on the following plan + + + You are managing the PTT Addon + + + In the number box below set the amount of 10 Concurrent Push-To-Talk User Packs you wish to purchase on a monthly basis. If you increase the number you will be billed immediately, if you decrease the number your renewal will be at the reduced count. If you set the zero you will not be billed (PTT won’t renew). The card on file for your standard Resgrid subscription will be used for this PTT Addon Subscription. + + + Manage Your Subscription + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.es.resx b/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.es.resx new file mode 100644 index 00000000..e97154c4 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.es.resx @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Éxito de actualización de facturación + + + Gracias por actualizar su información de facturación. Su nueva tarjeta no se ha cobrado en este momento, pero se usará cuando la suscripción se renueve en su momento normal. + + + Comprar complemento + + + Estás comprando el siguiente complemento + + + Su compra de complementos anteriores se agregará a su suscripción existente y se prorbe por el tiempo restante en su frecuencia de facturación actual. Se cobrará su tarjeta actual en el archivo, asegúrese de que esté actualizado para evitar una interrupción del servicio. + + + Advertencia + + + Ya tiene un complemento cancelado de este tipo con el tiempo que aún queda. Para evitar la doble facturación, espere hasta esta vez para agregar este complemento nuevamente. Si no desea esperar, puede agregar ahora y se puede prorratear alguna porción del tiempo restante. Tu tiempo restante termina en + + + Comprar ahora + + + Comprar ahora + + + Estás comprando el siguiente plan + + + para + + + año + + + mes + + + Su próximo pago estará en + + + Está cambiando de un plan anual a un plan mensual y retiene el crédito de su plan anual. Una vez que se use ese crédito, se le facturará en un horario mensual normal. + + + Su total de actualización estimada es + + + Costo estimado, el costo real puede ser ligeramente diferente. + + + Dirección de facturación de tarjeta de crédito + + + Billing Street Dirección de la tarjeta de crédito + + + Debe igualar la dirección de la calle donde se envían los estados de facturación de la tarjeta de crédito + + + Billing Zip/Código postal + + + País de facturación + + + La información de pago se asegura con el cifrado SSL. <b> regrid no almacena ninguna información de pago. </b> + + + Nombre en la tarjeta + + + Apellido en la tarjeta + + + Número de tarjeta de crédito completo + + + Número CVV/CVC + + + Número de 3 o 4 dígitos en la tarjeta + + + más información + + + Vencimiento de la tarjeta de crédito + + + Cancelar suscripción + + + Nos entristece ver que cancele su suscripción. Complete el siguiente formulario para completar su solicitud de cancelación de suscripción. El campo de la razón es opcional, asegúrese de verificar la casilla de confirmación y presione Aceptar en el mensaje de confirmación del navegador cuando aparezca. + + + Su departamento y todos sus datos no serán eliminados, solo se cancelará su renovación de suscripción. Para el tiempo restante en su suscripción, aún tendrá acceso a las características existentes parte de ese plan. + + + Plan + + + Plan actual + + + Comprado + + + Confirmar + + + Razón + + + Volver a suscripciones + + + Cancelar suscripción + + + ADVERTENCIA: Esto cancelará su suscripción activa. ¿Está seguro? + + + Cancelar falla + + + No pudimos cancelar su suscripción. Esto puede deberse a que no tiene una suscripción activa (es decir, está en el plan gratuito) o debido a un problema temporal con el procesador de pago. Vuelva a intentarlo en unos minutos. Si sigue obteniendo este problema, contáctenos para ayudarlo a resolverlo. + + + Suscripción cancelada + + + Su suscripción ha sido cancelada. El sistema puede tardar unos minutos en reflejar esto, pero su tarjeta ahora se cargará más. Mantendremos sus datos seguros en caso de que desee regresar o usar el plan de regrid gratuito. + + + Plan de suscripción y facturación + + + Ver historial de pago + + + Cambiar información de facturación + + + Cancelada + + + Activa + + + El plan expira + + + Renovaciones de plan + + + Personal + + + Grupos + + + Unidades + + + Advertencia + + + Estándar + + + De primera calidad + + + Profesional + + + Última + + + Empresa + + + Unificada + + + Mensajes de texto entrantes + + + Alerta telefónica + + + Precio + + + por año + + + por mes + + + Cantidad + + + Estás comprando el siguiente plan + + + Comprar mensualmente + + + También están disponibles planes más grandes, alojamiento privado, soporte prioritario, soporte de código abierto y servicios de implementación. + + + para más información. + + + Comprar anualmente + + + Si desea hacer el cambio en su suscripción, presione el botón "Cambiar Plan" a continuación. Nuestro proveedor de pagos calculará la proración del tiempo de su plan actual y determinará el saldo adeudado y cargará su tarjeta en el archivo (si es necesario). Recibirá un correo electrónico con los detalles una vez que se haya procesado el cambio. + + + Contáctenos + + + Costo + + + Cliente + + + Departamento + + + Descripción + + + Fecha final + + + Factura + + + Fecha de la factura + + + Método + + + Gracias por su compra. Su pago ahora completa su complemento o actualización del plan debe estar disponible pronto. Es posible que deba iniciar sesión y volver a iniciar sesión para ver los cambios. + + + Pago completo + + + Lo sentimos, pero hubo un problema tratando de procesar su pago. Presione el botón Atrás en su navegador y verifique su información de pago (es decir, código postal/postal, número CCV, vencimiento). + + + Pago fallido + + + historial de pagos + + + Debido al volumen más alto del esperado, su pago se encuentra actualmente en una cola. Puede dejar esta página y utilizar regrid como de costumbre. Una vez que hayamos procesado el pago, su cuenta se actualizará y recibirá un correo electrónico de nosotros. Si no recibe un correo electrónico de nosotros dentro de los 30 minutos, contáctenos en team@resgrid.com con su problema. + + + Pago pendiente + + + Gracias por su compra. Su pago se está procesando actualmente. Espere hasta que termine nuestro procesador de pago y su departamento se actualice. + + + Procesando pago + + + Paypal + + + Período + + + Procesador + + + Fecha de compra + + + Fecha de inicio + + + Raya + + + Gracias por actualizar su información de facturación. Su nueva tarjeta no se ha cobrado en este momento, pero se usará cuando la suscripción se renueve en su momento normal. + + + Éxito de actualización de facturación + + + Gracias por su compra. Su pago se está procesando actualmente. Espere hasta que Stripe, nuestro procesador de pagos, los acabados y su departamento se actualizarán. + + + Procesando pago + + + Sistema + + + ¡Gracias! + + + Total + + + Transacción + + + ID de transacción + + + Gracias por su interés en comprar un plan para el servicio de regrid. Ya tenemos un plan próximo para su departamento y solo permitimos 1 plan futuro por cliente. Si desea comprar varios años, siéntase libre de contactarnos y nos alegraría ayudarlo. + + + Incapaz de comprar + + + Próximo plan + + + Actualizar información de facturación + + + Cambio de plan + + + Mirar la factura + + + Actualmente estás en el siguiente plan + + + + + + Administre su suscripción + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.resx b/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.resx new file mode 100644 index 00000000..df94be13 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Subscription/Subscription.resx @@ -0,0 +1,23 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + + + + + + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Units/Units.cs b/Core/Resgrid.Localization/Areas/User/Units/Units.cs new file mode 100644 index 00000000..9c4e4b87 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Units/Units.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Units +{ + public class Units + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Units/Units.en.resx b/Core/Resgrid.Localization/Areas/User/Units/Units.en.resx new file mode 100644 index 00000000..868e0ffe --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Units/Units.en.resx @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Add Log + + + Add Role + + + Add Unit Log + + + Are you sure you want to permanently delete all statuses for this unit? + + + Clear out all Statuses For Unit + + + Delete All + + + WARNING: This will permanently delete this unit. Are you sure you want to delete the unit + + + Edit Unit + + + Events for + + + Generate Report + + + Log Body (Narrative) + + + A Narrative for the unit log + + + Logs + + + New Unit + + + Not Occupied + + + No Un-Grouped Units + + + No Units in Department + + + No Units in this Group + + + Remove this role + + + Role Name + + + Role Name + + + Select Unit + + + Set Status for Units + + + Set Status + + + Set Status for Selected Units + + + Set Unit Status + + + You have selected a unit, only other units with the same custom (or default) states can be selected. + + + Unit Staffing + + + Unit Staffing + + + View Events + + + View Unit Events + + + View Unit Logs + + + Yes I'm sure + + + You can add a new Type + + + here + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Units/Units.es.resx b/Core/Resgrid.Localization/Areas/User/Units/Units.es.resx new file mode 100644 index 00000000..b73da071 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Units/Units.es.resx @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Agregar registro + + + Agregar rol + + + Agregar registro de unidades + + + ¿Estás seguro de que quieres eliminar permanentemente todos los estados para esta unidad? + + + Borrar todos los estados para la unidad + + + Eliminar todos + + + ADVERTENCIA: Esto eliminará permanentemente esta unidad. ¿Estás seguro de que quieres eliminar la unidad? + + + Unidad de edición + + + Eventos para + + + Generar informe + + + Cuerpo de registro (narrativa) + + + Una narrativa para el registro de la unidad + + + Registro + + + Nueva unidad + + + Desocupado + + + No hay unidades no agrupadas + + + No hay unidades en el departamento + + + No hay unidades en este grupo + + + Eliminar este papel + + + Nombre de rol + + + Nombre de rol + + + Unidad de selección + + + Establecer el estado de las unidades + + + Establecer el estado + + + Estado establecido para unidades seleccionadas + + + Establecer el estado de la unidad + + + Ha seleccionado una unidad, solo se pueden seleccionar otras unidades con los mismos estados personalizados (o predeterminados). + + + Personal de la unidad + + + Personal de la unidad + + + Ver eventos + + + Ver eventos de la unidad + + + Ver registros de unidades + + + Sí estoy seguro + + + Puedes agregar un nuevo tipo + + + aquí + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Voice/Voice.cs b/Core/Resgrid.Localization/Areas/User/Voice/Voice.cs new file mode 100644 index 00000000..befa80d4 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Voice/Voice.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization.Areas.User.Voice +{ + public class Voice + { + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Voice/Voice.en.resx b/Core/Resgrid.Localization/Areas/User/Voice/Voice.en.resx new file mode 100644 index 00000000..dceb2cf9 --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Voice/Voice.en.resx @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + New Audio Stream + + + Add Audio Stream + + + New Voice Channel + + + Audio Stream Name + + + Audio Streams + + + Audio Stream Url + + + Channel Name + + + Edit Audio Stream + + + Is Default + + + Participants + + + Push-To-Talk Addon Not Purchased + + + Your department has not purchased the Push-To-Talk addon. Your departments Managing Member can purchase the addon from the Subscription and Billing page. Push-To-Talk allows your department to enable real-time voice communications though the Resgrid applications. + + + Save Audio Stream + + + Voice (PTT) Channels + + + Audio & Push-To-Talk + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Areas/User/Voice/Voice.es.resx b/Core/Resgrid.Localization/Areas/User/Voice/Voice.es.resx new file mode 100644 index 00000000..4fdb1b6a --- /dev/null +++ b/Core/Resgrid.Localization/Areas/User/Voice/Voice.es.resx @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Common.cs b/Core/Resgrid.Localization/Common.cs new file mode 100644 index 00000000..8cd31833 --- /dev/null +++ b/Core/Resgrid.Localization/Common.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Localization +{ + public class Common + { + } +} diff --git a/Core/Resgrid.Localization/Common.en.resx b/Core/Resgrid.Localization/Common.en.resx new file mode 100644 index 00000000..0f7070d9 --- /dev/null +++ b/Core/Resgrid.Localization/Common.en.resx @@ -0,0 +1,651 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Action + + + Actions + + + Activate + + + Active + + + Archived Calls + + + Attachment + + + Attachments + + + Audio & Push-To-Talk + + + Available + + + Available at + + + Available Station + + + Big Board + + + Blog + + + Calendar + + + Call + + + Call Address + + + Call Import Settings + + + Calls + + + Call Timestamp + + + Cancel + + + Canceled + + + Certifications + + + Close + + + Closed + + + Code + + + Color + + + Commands + + + Committed + + + Connect + + + Contact Us + + + Custom Statuses + + + Data + + + Day + + + Days + + + Deactivate + + + Default + + + Delayed + + + Delete + + + Department + + + Department Settings + + + Description + + + Discard + + + Dispatched + + + Distribution Lists + + + Documents + + + Documents + + + Download + + + End + + + Eta + + + Event + + + Expired + + + Export + + + Files + + + Forms + + + Friday + + + General + + + Groups + + + Help + + + Help & Support + + + Home + + + Hour + + + Hours + + + Id + + + Images + + + Info + + + In Quarters + + + Instructors + + + Inventory + + + Knowledge Base + + + Language + + + Links + + + Location + + + Logout + + + Logs + + + Mailbox + + + Map + + + Mapping + + + Maybe + + + Minutes + + + Monday + + + Monthly + + + Name + + + Name + + + Nature of Call + + + Never + + + No + + + None + + + Note + + + Notes + + + Notes + + + Notifications + + + Not Responding + + + Number + + + On Scene + + + On Shift + + + Optional + + + Options + + + Order + + + Orders + + + Personnel + + + Personnel + + + Preview + + + Print + + + Priority + + + Process + + + Profile + + + Protocols + + + Push-To-Talk + + + Released + + + Remove + + + Repeat + + + Reports + + + Required + + + Responding + + + Responding to + + + Respond to a Call + + + Respond to a Station + + + Roles + + + Route + + + Saturday + + + Save + + + Saving + + + Security & Permissions + + + Send + + + Shifts + + + Signup + + + Size + + + Staffing + + + Standing By + + + Standing By at Station + + + Start + + + Station + + + Stations & Groups + + + Status + + + Status + + + New Call Form + + + Subscription & Billing + + + Sunday + + + System + + + Templates + + + Text + + + Text Messaging + + + Thursday + + + Timestamp + + + Title + + + Training + + + Course\Training Code + + + Course\Training + + + Trainings + + + Tuesday + + + Type + + + Types + + + Unavailable + + + Unfounded + + + Units + + + Units + + + Unknown + + + Update + + + Upgrade Your Plan + + + Uploaded By + + + Uploaded On + + + Users + + + Valid + + + Video + + + Videos + + + View + + + See All Calls + + + View Your Message Inbox + + + Warning + + + Wednesday + + + Week + + + Weekly + + + Weeks + + + Work Log + + + Yearly + + + Yes + + + Your Departments + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Common.es.resx b/Core/Resgrid.Localization/Common.es.resx new file mode 100644 index 00000000..7dda65cd --- /dev/null +++ b/Core/Resgrid.Localization/Common.es.resx @@ -0,0 +1,648 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Acción + + + Comportamiento + + + Activar + + + Activo + + + Llamadas archivadas + + + Adjunto + + + Archivos adjuntos + + + Disponible + + + Disponible en + + + Estación disponible + + + tablero grande + + + Blog + + + Calendario + + + Llamar + + + Dirección de llamada + + + Configuración de importación de llamadas + + + Llamadas + + + Marca de tiempo de la llamada + + + Cancelar + + + Cancelado + + + Certificaciones + + + Cerca + + + Cerrado + + + Código + + + Color + + + Comandos + + + Comprometido + + + Conectar + + + Contáctenos + + + Estados personalizados + + + Datos + + + Día + + + Días + + + Desactivar + + + Por defecto + + + Demorado + + + Borrar + + + Departamento + + + Configuración del departamento + + + Descripción + + + Desechar + + + Enviado + + + Listas de distribución + + + Documentos + + + Documentos + + + Descargar + + + Fin + + + eta + + + Evento + + + Venció + + + Exportar + + + archivos + + + Formularios + + + Viernes + + + General + + + Grupos + + + Ayuda + + + Servicio de asistencia + + + Hogar + + + Hora + + + Horas + + + Identificación + + + Imágenes + + + Información + + + en cuartos + + + instructores + + + Inventario + + + Base de conocimientos + + + Idioma + + + Enlaces + + + Ubicación + + + Cerrar sesión + + + Registros + + + Buzón + + + Mapa + + + Cartografía + + + Tal vez + + + Minutos + + + Lunes + + + Mensual + + + Nombre + + + Nombre + + + Naturaleza de la llamada + + + Nunca + + + No + + + Ninguno + + + Nota + + + notas + + + notas + + + Notificaciones + + + No responde + + + Número + + + En escena + + + En el turno + + + Opcional + + + Opciones + + + Orden + + + Pedidos + + + Personal + + + Personal + + + Avance + + + Imprimir + + + Prioridad + + + Proceso + + + Perfil + + + Protocolos + + + Pulsar para hablar + + + Liberado + + + Eliminar + + + Repetir + + + Informes + + + Requerido + + + respondiendo + + + Respondiendo a + + + Responder a una llamada + + + Responder a una estación + + + roles + + + Ruta + + + Sábado + + + Ahorrar + + + Ahorro + + + Seguridad y permisos + + + Enviar + + + Turnos + + + Inscribirse + + + Tamaño + + + dotación de personal + + + En espera + + + En espera en la estación + + + Comenzar + + + Estación + + + Estaciones y Grupos + + + Estado + + + Estado + + + Formulario de nueva convocatoria + + + Suscripción y facturación + + + Domingo + + + Sistema + + + Plantillas + + + Texto + + + Mensaje de texto + + + Jueves + + + marca de tiempo + + + Título + + + Capacitación + + + Curso\Código de entrenamiento + + + Curso\Formación + + + Entrenamientos + + + Martes + + + Tipo + + + Tipos + + + Indisponible + + + Infundado + + + Unidades + + + Unidades + + + Desconocido + + + Actualizar + + + Actualice su plan + + + Subido por + + + Subido en + + + Usuarios + + + Válido + + + Video + + + Vídeos + + + Vista + + + Ver todas las llamadas + + + Ver su bandeja de entrada de mensajes + + + Advertencia + + + Miércoles + + + Semana + + + Semanalmente + + + Semanas + + + Registro de trabajo + + + Anual + + + + + + Tus Departamentos + + \ No newline at end of file diff --git a/Core/Resgrid.Localization/Resgrid.Localization.csproj b/Core/Resgrid.Localization/Resgrid.Localization.csproj new file mode 100644 index 00000000..0b6c7f5d --- /dev/null +++ b/Core/Resgrid.Localization/Resgrid.Localization.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.1 + enable + + + + + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + + + ResXFileCodeGenerator + Subscription.en.Designer.cs + + + + diff --git a/Core/Resgrid.Localization/SupportedLocales.cs b/Core/Resgrid.Localization/SupportedLocales.cs new file mode 100644 index 00000000..5e096f97 --- /dev/null +++ b/Core/Resgrid.Localization/SupportedLocales.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Resgrid.Localization +{ + public static class SupportedLocales + { + public static Dictionary SupportedLanguagesMap = new Dictionary() + { + {"en", "English (United States)"}, //"en-US" + {"es", "Spanish (Latin America)"}, //"es-MX" + }; + + public static string[] GetSupportedCultures() + { + return SupportedLanguagesMap.Select(x => x.Key).ToArray(); + } + + public static List GetSupportedCultureInfos() + { + return SupportedLanguagesMap.Select(x => new CultureInfo(x.Key)).ToList(); + } + + public static List DefaultENCultureInfos() + { + var cultures = new List(); + cultures.Add(new CultureInfo("en")); + + return cultures; + } + } +} diff --git a/Core/Resgrid.Model/AuditLogTypes.cs b/Core/Resgrid.Model/AuditLogTypes.cs index 69155585..75884ac5 100644 --- a/Core/Resgrid.Model/AuditLogTypes.cs +++ b/Core/Resgrid.Model/AuditLogTypes.cs @@ -18,6 +18,10 @@ public enum AuditLogTypes SubscriptionCancelled, SubscriptionBillingInfoUpdated, // New - CallReactivated + CallReactivated, + UserAccountDeleted, + AddonSubscriptionModified, + DeleteDepartmentRequested, + DeleteDepartmentRequestedCancelled } } diff --git a/Core/Resgrid.Model/Call.cs b/Core/Resgrid.Model/Call.cs index c37da40a..4c6f374e 100644 --- a/Core/Resgrid.Model/Call.cs +++ b/Core/Resgrid.Model/Call.cs @@ -133,6 +133,9 @@ public class Call : IEntity [ProtoMember(30)] public virtual ICollection Protocols { get; set; } + [ProtoMember(31)] + public virtual ICollection References { get; set; } + public string ContactName { get; set; } public string ContactNumber { get; set; } @@ -187,7 +190,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" }; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "ReportingUser", "ClosedByUser", "Department", "Dispatches", "Attachments", "CallNotes", "GroupDispatches", "UnitDispatches", "RoleDispatches", "Protocols", "ShortenedAudioUrl", "ShortenedCallUrl", "CallPriority", "PreviousDispatchCount", "References" }; public string GetIdentifier() { @@ -269,6 +272,23 @@ public string GetPriorityText() return "Unknown"; } + public string GetStateText() + { + switch (State) + { + case (int)CallStates.Active: + return "Active"; + case (int)CallStates.Cancelled: + return "Cancelled"; + case (int)CallStates.Closed: + return "Closed"; + case (int)CallStates.Unfounded: + return "Unfounded"; + } + + return "Unknown"; + } + public bool HasAnyDispatches() { if (Dispatches != null && Dispatches.Any()) diff --git a/Core/Resgrid.Model/CallReference.cs b/Core/Resgrid.Model/CallReference.cs new file mode 100644 index 00000000..d6863931 --- /dev/null +++ b/Core/Resgrid.Model/CallReference.cs @@ -0,0 +1,68 @@ +using ProtoBuf; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; +using System.Text.Json.Serialization; + +namespace Resgrid.Model +{ + /// + /// Class CallReference. + /// Implements the + /// + /// + [ProtoContract] + public class CallReference : IEntity + { + [ProtoMember(1)] + public string CallReferenceId { get; set; } + + /// + /// The call that is being linked from + /// + [ProtoMember(2)] + public int SourceCallId { get; set; } + + [ProtoMember(8)] + public Call SourceCall { get; set; } + + /// + /// The call that is being linked too (i.e. parent) + /// + [ProtoMember(3)] + public int TargetCallId { get; set; } + + [ProtoMember(7)] + public Call TargetCall { get; set; } + + [ProtoMember(4)] + public string AddedByUserId { get; set; } + + [ProtoMember(5)] + public DateTime AddedOn { get; set; } + + [ProtoMember(6)] + public string Note { get; set; } + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return CallReferenceId; } + set { CallReferenceId = (string)value; } + } + + [NotMapped] + public string TableName => "CallReferences"; + + [NotMapped] + public string IdName => "CallReferenceId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "TargetCall", "SourceCall" }; + } +} diff --git a/Core/Resgrid.Model/DeleteGroupResults.cs b/Core/Resgrid.Model/DeleteGroupResults.cs index 455fc847..279f15a1 100644 --- a/Core/Resgrid.Model/DeleteGroupResults.cs +++ b/Core/Resgrid.Model/DeleteGroupResults.cs @@ -3,6 +3,13 @@ public enum DeleteGroupResults { NoFailure, - UnAuthroized + UnAuthorized + } + + public enum DeleteDepartmentResults + { + NoFailure, + UnAuthorized, + Failure } } diff --git a/Core/Resgrid.Model/DepartmentAudio.cs b/Core/Resgrid.Model/DepartmentAudio.cs new file mode 100644 index 00000000..e8ff59fa --- /dev/null +++ b/Core/Resgrid.Model/DepartmentAudio.cs @@ -0,0 +1,49 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; + +namespace Resgrid.Model +{ + public class DepartmentAudio : IEntity + { + public string DepartmentAudioId { get; set; } + + public int DepartmentId { get; set; } + + public virtual Department Department { get; set; } + + public int DepartmentAudioType { get; set; } + + public string Name { get; set; } + + public string Data { get; set; } + + public string Type { get; set; } + + public DateTime AddedOn { get; set; } + + public string AddedByUserId { get; set; } + + [NotMapped] + [JsonIgnore] + public object IdValue + { + get { return DepartmentAudioId; } + set { DepartmentAudioId = (string)value; } + } + + [NotMapped] + public string TableName => "DepartmentAudios"; + + [NotMapped] + public string IdName => "DepartmentAudioId"; + + [NotMapped] + public int IdType => 1; + + [NotMapped] + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "Department" }; + } +} diff --git a/Core/Resgrid.Model/DepartmentSettingTypes.cs b/Core/Resgrid.Model/DepartmentSettingTypes.cs index dfa427c0..6dd55754 100644 --- a/Core/Resgrid.Model/DepartmentSettingTypes.cs +++ b/Core/Resgrid.Model/DepartmentSettingTypes.cs @@ -27,6 +27,7 @@ public enum DepartmentSettingTypes AutoSetStatusForShiftDispatchPersonnel = 23, ShiftCallDispatchPersonnelStatusToSet = 24, ShiftCallReleasePersonnelStatusToSet = 25, - AllowSignupsForMultipleShiftGroups = 26 + AllowSignupsForMultipleShiftGroups = 26, + StaffingSuppressStaffingLevels = 27 } } diff --git a/Core/Resgrid.Model/DepartmentSuppressStaffingInfo.cs b/Core/Resgrid.Model/DepartmentSuppressStaffingInfo.cs new file mode 100644 index 00000000..fa4c5efb --- /dev/null +++ b/Core/Resgrid.Model/DepartmentSuppressStaffingInfo.cs @@ -0,0 +1,20 @@ +using ProtoBuf; +using System.Collections.Generic; + +namespace Resgrid.Model +{ + [ProtoContract] + public class DepartmentSuppressStaffingInfo + { + [ProtoMember(1)] + public bool EnableSupressStaffing { get; set; } + + [ProtoMember(2)] + public List StaffingLevelsToSupress { get; set; } + + public DepartmentSuppressStaffingInfo() + { + StaffingLevelsToSupress = new List(); + } + } +} diff --git a/Core/Resgrid.Model/DepartmentVoiceUtilization.cs b/Core/Resgrid.Model/DepartmentVoiceUtilization.cs new file mode 100644 index 00000000..7e2a2325 --- /dev/null +++ b/Core/Resgrid.Model/DepartmentVoiceUtilization.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Model +{ + public class DepartmentVoiceUtilization + { + public int SeatLimit { get; set; } + public int CurrentlyActive { get; set; } + } +} diff --git a/Core/Resgrid.Model/Events/NewChatNotificationEvent.cs b/Core/Resgrid.Model/Events/NewChatNotificationEvent.cs index 204446a1..8889d64f 100644 --- a/Core/Resgrid.Model/Events/NewChatNotificationEvent.cs +++ b/Core/Resgrid.Model/Events/NewChatNotificationEvent.cs @@ -23,5 +23,8 @@ public class NewChatNotificationEvent [ProtoMember(6)] public int Type { get; set; } + + [ProtoMember(7)] + public int DepartmentId { get; set; } } } diff --git a/Core/Resgrid.Model/Facades/Stripe/IStripeSubscriptionServiceFacade.cs b/Core/Resgrid.Model/Facades/Stripe/IStripeSubscriptionServiceFacade.cs index 4ec36da7..cb1a4637 100644 --- a/Core/Resgrid.Model/Facades/Stripe/IStripeSubscriptionServiceFacade.cs +++ b/Core/Resgrid.Model/Facades/Stripe/IStripeSubscriptionServiceFacade.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Stripe; @@ -9,7 +10,7 @@ public interface IStripeSubscriptionServiceFacade Task Get(string customerId, string subscriptionId); Task Create(string customerId, string planId, SubscriptionCreateOptions createOptions = null); Task Update(string customerId, string subscriptionId, SubscriptionUpdateOptions updateOptions); - Subscription Cancel(string customerId, string subscriptionId, bool cancelAtPeriodEnd = false); + Task CancelAsync(string customerId, string subscriptionId, bool cancelAtPeriodEnd = false, CancellationToken cancellationToken = default(CancellationToken)); Task> List(string customerId, ListOptions listOptions = null); Task GetCurrentActiveSubAsync(string customerId); Task AddAddonToSubscription(string customerId, Model.Plan plan, PlanAddon addon); @@ -17,5 +18,7 @@ public interface IStripeSubscriptionServiceFacade Task GetCurrentActiveOrCanceledSubAsync(string customerId); Task GetCurrentCanceledSubAsync(string customerId); Task GetCurrentCanceledSubWithAddonAsync(string customerId, PlanAddon addon); + Task AdjustPTTAddonQuantityToSubscription(Subscription sub, long quantity, PlanAddon addon, CancellationToken cancellationToken = default(CancellationToken)); + Task CreatePTTAddonSubscription(string customerId, long quantity, PlanAddon addon, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Core/Resgrid.Model/Payment.cs b/Core/Resgrid.Model/Payment.cs index ee90c1d4..0da212fa 100644 --- a/Core/Resgrid.Model/Payment.cs +++ b/Core/Resgrid.Model/Payment.cs @@ -137,5 +137,13 @@ public DateTime GetEndDate() return DateTime.MinValue; } + + public bool IsFreePlan() + { + if (PlanId == 1) + return true; + + return false; + } } } diff --git a/Core/Resgrid.Model/PaymentAddon.cs b/Core/Resgrid.Model/PaymentAddon.cs index 02b45446..be012653 100644 --- a/Core/Resgrid.Model/PaymentAddon.cs +++ b/Core/Resgrid.Model/PaymentAddon.cs @@ -67,6 +67,9 @@ public class PaymentAddon : IEntity [ProtoMember(23)] public string SubscriptionId { get; set; } + [ProtoMember(24)] + public long Quantity { get; set; } + [NotMapped] [JsonIgnore] public object IdValue diff --git a/Core/Resgrid.Model/PlanAddon.cs b/Core/Resgrid.Model/PlanAddon.cs index 4a0e27f0..9986945e 100644 --- a/Core/Resgrid.Model/PlanAddon.cs +++ b/Core/Resgrid.Model/PlanAddon.cs @@ -10,7 +10,7 @@ public class PlanAddon : IEntity { public string PlanAddonId { get; set; } - public int PlanId { get; set; } + public int? PlanId { get; set; } public virtual Plan Plan { get; set; } @@ -20,10 +20,17 @@ public class PlanAddon : IEntity public string ExternalId { get; set; } + public string TestExternalId { get; set; } + + [NotMapped] public bool IsCancelled { get; set; } + [NotMapped] public DateTime? EndingOn { get; set; } + [NotMapped] + public long Quantity { get; set; } + [NotMapped] [JsonIgnore] public object IdValue @@ -42,7 +49,7 @@ public object IdValue public int IdType => 1; [NotMapped] - public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "Plan", "IsCancelled", "EndingOn" }; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "Plan", "IsCancelled", "EndingOn", "Quantity" }; public DateTime GetEndDateFromNow() { @@ -61,7 +68,8 @@ public DateTime GetEndDateFromNow() } } - return DateTime.MinValue; + // No plan, which means no plan frequency, so default to 1 month. + return DateTime.UtcNow.AddMonths(1).AddDays(7).SetToEndOfDay(); } public string GetAddonName() diff --git a/Core/Resgrid.Model/Providers/IEmailProvider.cs b/Core/Resgrid.Model/Providers/IEmailProvider.cs index aedab18a..c81501f5 100644 --- a/Core/Resgrid.Model/Providers/IEmailProvider.cs +++ b/Core/Resgrid.Model/Providers/IEmailProvider.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace Resgrid.Model.Providers { @@ -38,5 +39,9 @@ Task SendUpgradePaymentReciept(string departmentName, string processDate, Task SendChargeFailed(string name, string email, string endDate, string departmentName, string planName); Task SendNewDepartmentLinkMail(string name, string departmentName, string data, string email, int departmentId); Task SendTroubleAlertMail(string email, string unitName, string gpsLocation, string personnel, string callAddress, string unitAddress, string dispatchedOn, string callName); + + + // Internal Template Only Emails + Task SendDeleteDepartmentEmail(string requesterName, string departmentName, DateTime localCompletedOn, string sendingToPersonName, string email); } } diff --git a/Core/Resgrid.Model/QueueItem.cs b/Core/Resgrid.Model/QueueItem.cs index aa031cf4..7038c2d4 100644 --- a/Core/Resgrid.Model/QueueItem.cs +++ b/Core/Resgrid.Model/QueueItem.cs @@ -21,7 +21,7 @@ public class QueueItem : IEntity public int QueueType { get; set; } [ProtoMember(3)] - public string SourceId { get; set; } + public string SourceId { get; set; } // UserId or DepartmentId [ProtoMember(4)] public DateTime QueuedOn { get; set; } @@ -32,9 +32,24 @@ public class QueueItem : IEntity [ProtoMember(6)] public DateTime? CompletedOn { get; set; } + [ProtoMember(8)] + public DateTime? ToBeCompletedOn { get; set; } + [ProtoMember(7)] public string Receipt { get; set; } + [ProtoMember(9)] + public string Reason { get; set; } + + [ProtoMember(10)] + public string QueuedByUserId { get; set; } + + [ProtoMember(11)] + public string Data { get; set; } + + [ProtoMember(12)] + public int ReminderCount { get; set; } + [NotMapped] public int DequeueCount { get; set; } @@ -56,6 +71,6 @@ public object IdValue public int IdType => 0; [NotMapped] - public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "DequeueCount" }; } } diff --git a/Core/Resgrid.Model/QueueTypes.cs b/Core/Resgrid.Model/QueueTypes.cs index ce9bddd8..18c216b7 100644 --- a/Core/Resgrid.Model/QueueTypes.cs +++ b/Core/Resgrid.Model/QueueTypes.cs @@ -2,8 +2,10 @@ { public enum QueueTypes { - MessageBroadcast, - CallBroadcast, - DistributionListBroadcast + MessageBroadcast, // OLD + CallBroadcast, // OLD + DistributionListBroadcast, // OLD + DeleteAccount, // System Queue Item + DeleteDepartment // System Queue Item } -} \ No newline at end of file +} diff --git a/Core/Resgrid.Model/Repositories/ICallReferencesRepository.cs b/Core/Resgrid.Model/Repositories/ICallReferencesRepository.cs new file mode 100644 index 00000000..d58f942b --- /dev/null +++ b/Core/Resgrid.Model/Repositories/ICallReferencesRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + /// + /// Interface ICallReferencesRepository + /// Implements the + /// + /// + public interface ICallReferencesRepository : IRepository + { + Task> GetCallReferencesByTargetCallIdAsync(int callId); + Task> GetCallReferencesBySourceCallIdAsync(int callId); + } +} diff --git a/Core/Resgrid.Model/Repositories/IDeleteRepository.cs b/Core/Resgrid.Model/Repositories/IDeleteRepository.cs new file mode 100644 index 00000000..61496a27 --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IDeleteRepository.cs @@ -0,0 +1,19 @@ +using Resgrid.Model.Identity; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + /// + /// Interface IDeleteRepository + /// + public interface IDeleteRepository + { + /// + /// Deletes a Department + /// + /// Boolean, true if successful + Task DeleteDepartmentAndUsersAsync(int departmentId); + } +} diff --git a/Core/Resgrid.Model/Repositories/IDepartmentAudioRepository.cs b/Core/Resgrid.Model/Repositories/IDepartmentAudioRepository.cs new file mode 100644 index 00000000..85e3fd78 --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IDepartmentAudioRepository.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + /// + /// Interface IDepartmentAudioRepository + /// Implements the + /// + /// + public interface IDepartmentAudioRepository : IRepository + { + } +} diff --git a/Core/Resgrid.Model/Repositories/IIdentityRepository.cs b/Core/Resgrid.Model/Repositories/IIdentityRepository.cs index aa45bb31..ae12a82c 100644 --- a/Core/Resgrid.Model/Repositories/IIdentityRepository.cs +++ b/Core/Resgrid.Model/Repositories/IIdentityRepository.cs @@ -134,5 +134,9 @@ public interface IIdentityRepository Task> GetAllUsersForDepartmentWithinLimitsAsync(int departmentId, bool retrieveHidden); Task CleanUpOIDCTokensAsync(DateTime timestamp); + + Task ClearOutUserLoginAsync(string userId); + + Task CleanUpOIDCTokensByUserAsync(string userId); } } diff --git a/Core/Resgrid.Model/Services/IAddressService.cs b/Core/Resgrid.Model/Services/IAddressService.cs index 2a1cfd60..a3308c90 100644 --- a/Core/Resgrid.Model/Services/IAddressService.cs +++ b/Core/Resgrid.Model/Services/IAddressService.cs @@ -9,5 +9,6 @@ public interface IAddressService Task
GetAddressByIdAsync(int addressId); Task
SaveAddressAsync(Address address, CancellationToken cancellationToken = default(CancellationToken)); Task IsAddressValidAsync(Address address); + Task DeleteAddress(int addressId, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Core/Resgrid.Model/Services/IAuthorizationService.cs b/Core/Resgrid.Model/Services/IAuthorizationService.cs index 1f0d1139..35325068 100644 --- a/Core/Resgrid.Model/Services/IAuthorizationService.cs +++ b/Core/Resgrid.Model/Services/IAuthorizationService.cs @@ -272,5 +272,7 @@ public interface IAuthorizationService Task CanUserCloseCallAsync(string userId, int callId, int departmentId); Task CanUserAddCallDataAsync(string userId, int callId, int departmentId); + + Task CanUserDeleteDepartmentAsync(string userId, int departmentId); } } diff --git a/Core/Resgrid.Model/Services/ICallsService.cs b/Core/Resgrid.Model/Services/ICallsService.cs index df4782e8..4d1e77cc 100644 --- a/Core/Resgrid.Model/Services/ICallsService.cs +++ b/Core/Resgrid.Model/Services/ICallsService.cs @@ -34,7 +34,7 @@ Task RegenerateCallNumbersAsync(int departmentId, int year, ///
/// The department identifier. /// Task<System.String>. - Task GetCurrentCallNumberAsync(int departmentId); + Task GetCurrentCallNumberAsync(DateTime utcDate, int departmentId); /// /// Gets all calls by department asynchronous. @@ -396,12 +396,17 @@ Task ClearGroupForDispatchesAsync(int departmentGroupId, /// if set to true [get unit dispatches]. /// if set to true [get role dispatches]. /// 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); + Task PopulateCallData(Call call, bool getDispatches, bool getAttachments, bool getNotes, bool getGroupDispatches, bool getUnitDispatches, bool getRoleDispatches, bool getProtocols, bool getReferences); Task> GetAllNonDispatchedScheduledCallsWithinDateRange(DateTime startDate, DateTime endDate); Task> GetAllNonDispatchedScheduledCallsByDepartmentIdAsync(int departmentId); + + Task> GetChildCallsForCallAsync(int callId); + + Task DeleteCallReferenceAsync(CallReference callReference, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Core/Resgrid.Model/Services/ICommunicationService.cs b/Core/Resgrid.Model/Services/ICommunicationService.cs index fa90bb3d..08b36cf5 100644 --- a/Core/Resgrid.Model/Services/ICommunicationService.cs +++ b/Core/Resgrid.Model/Services/ICommunicationService.cs @@ -68,8 +68,7 @@ Task SendNotificationAsync(string userId, int departmentId, string message /// The sending user. /// The recipients. /// Task<System.Boolean>. - Task SendChat(string chatId, string sendingUserId, string group, string message, UserProfile sendingUser, - List recipients); + Task SendChat(string chatId, int departmentId, string sendingUserId, string group, string message, UserProfile sendingUser, List recipients); /// /// Sends the trouble alert asynchronous. diff --git a/Core/Resgrid.Model/Services/IDeleteService.cs b/Core/Resgrid.Model/Services/IDeleteService.cs index 86075d36..ce79491d 100644 --- a/Core/Resgrid.Model/Services/IDeleteService.cs +++ b/Core/Resgrid.Model/Services/IDeleteService.cs @@ -24,5 +24,12 @@ public interface IDeleteService /// The current user identifier. /// Task<DeleteGroupResults>. Task DeleteGroupAsync(int departmentGroupId, int departmentId, string currentUserId, CancellationToken cancellationToken = default(CancellationToken)); + + Task DeleteUserAccountAsync(int departmentId, string authorizingUserId, string userIdToDelete, string ipAddress, string userAgent, CancellationToken cancellationToken = default(CancellationToken)); + + Task DeleteDepartment(int departmentId, string authorizingUserId, string ipAddress, string userAgent, + CancellationToken cancellationToken = default(CancellationToken)); + + Task HandlePendingDepartmentDeletionRequestAsync(QueueItem item, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Core/Resgrid.Model/Services/IDepartmentGroupsService.cs b/Core/Resgrid.Model/Services/IDepartmentGroupsService.cs index 39795953..2488cefe 100644 --- a/Core/Resgrid.Model/Services/IDepartmentGroupsService.cs +++ b/Core/Resgrid.Model/Services/IDepartmentGroupsService.cs @@ -220,5 +220,7 @@ Task MoveUserIntoGroupAsync(string userId, int groupId, b List GetAllUsersForGroup(int groupId); Task DeleteGroupMembersByGroupIdAsync(int groupId, int departmentId, CancellationToken cancellationToken = default(CancellationToken)); + + Task> GetAllGroupsForDepartmentUnlimitedThinAsync(int departmentId); } } diff --git a/Core/Resgrid.Model/Services/IDepartmentSettingsService.cs b/Core/Resgrid.Model/Services/IDepartmentSettingsService.cs index a1d97eca..7451555e 100644 --- a/Core/Resgrid.Model/Services/IDepartmentSettingsService.cs +++ b/Core/Resgrid.Model/Services/IDepartmentSettingsService.cs @@ -255,5 +255,7 @@ public interface IDepartmentSettingsService Task GetShiftCallReleasePersonnelStatusToSetAsync(int departmentId); Task GetAllowSignupsForMultipleShiftGroupsAsync(int departmentId); + + Task GetDepartmentStaffingSuppressInfoAsync(int departmentId, bool bypassCache = false); } } diff --git a/Core/Resgrid.Model/Services/IEmailService.cs b/Core/Resgrid.Model/Services/IEmailService.cs index 4ed284b1..d3ee8a82 100644 --- a/Core/Resgrid.Model/Services/IEmailService.cs +++ b/Core/Resgrid.Model/Services/IEmailService.cs @@ -199,5 +199,7 @@ Task SendUserCancellationNotificationToTeamAsync(Department department, Pa /// The profile. /// Task<System.Boolean>. Task SendCalendarAsync(string userId, string message, int departmentId, UserProfile profile = null); + + Task SendDeleteDepartmentEmail(string sendingToEmail, string sendingToName, QueueItem queueItem); } } diff --git a/Core/Resgrid.Model/Services/IPaymentProviderService.cs b/Core/Resgrid.Model/Services/IPaymentProviderService.cs index b46e920b..64e0d4c8 100644 --- a/Core/Resgrid.Model/Services/IPaymentProviderService.cs +++ b/Core/Resgrid.Model/Services/IPaymentProviderService.cs @@ -45,8 +45,9 @@ Task ProcessStripeSubscriptionCancellationAsync(Subscription stripeSubs /// Processes the stripe subscription refund asynchronous. /// /// The stripe charge. + /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Task<Payment>. - Task ProcessStripeSubscriptionRefundAsync(Charge stripeCharge); + Task ProcessStripeSubscriptionRefundAsync(Charge stripeCharge, CancellationToken cancellationToken = default(CancellationToken)); /// /// Processes the stripe charge failed asynchronous. @@ -64,9 +65,10 @@ Task ProcessStripeSubscriptionCancellationAsync(Subscription stripeSubs /// The plan identifier. /// The email address. /// Name of the department. + /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Session. - Task CreateStripeSessionForSub(int departmentId, string stripeCustomerId, string stripePlanId, int planId, - string emailAddress, string departmentName); + Task CreateStripeSessionForSub(int departmentId, string stripeCustomerId, string stripePlanId, int planId, string emailAddress, string departmentName, + CancellationToken cancellationToken = default(CancellationToken)); /// /// Creates the stripe session for update. @@ -75,9 +77,10 @@ Task CreateStripeSessionForSub(int departmentId, string stripeCustomerI /// The stripe customer identifier. /// The email address. /// Name of the department. + /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Session. - Task CreateStripeSessionForUpdate(int departmentId, string stripeCustomerId, string emailAddress, - string departmentName); + Task CreateStripeSessionForUpdate(int departmentId, string stripeCustomerId, string emailAddress, string departmentName, + CancellationToken cancellationToken = default(CancellationToken)); /// /// Processes the stripe checkout completed asynchronous. @@ -92,16 +95,18 @@ Task ProcessStripeCheckoutCompletedAsync(Session session, /// Gets the active stripe subscription. /// /// The stripe customer identifier. + /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Subscription. - Subscription GetActiveStripeSubscription(string stripeCustomerId); + Task GetActiveStripeSubscriptionAsync(string stripeCustomerId, CancellationToken cancellationToken = default(CancellationToken)); /// /// Changes the active subscription. /// /// The stripe customer identifier. /// The stripe plan identifier. + /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Invoice. - Invoice ChangeActiveSubscription(string stripeCustomerId, string stripePlanId); + Task ChangeActiveSubscriptionAsync(string stripeCustomerId, string stripePlanId, CancellationToken cancellationToken = default(CancellationToken)); /// /// Processes the stripe checkout update. @@ -113,5 +118,13 @@ Task ProcessStripeCheckoutUpdateAsync(Session session, CancellationToken cancellationToken = default(CancellationToken)); Task ProcessStripeInvoicePaidAsync(Invoice invoice); + + Task ModifyPTTAddonSubscription(string stripeCustomerId, long quantity, PlanAddon addon, CancellationToken cancellationToken = default(CancellationToken)); + + Task CreateStripeSessionForCustomerPortal(int departmentId, string stripeCustomerId, string customerConfigId, string emailAddress, string departmentName, CancellationToken cancellationToken = default(CancellationToken)); + + Task GetActivePTTStripeSubscriptionAsync(string stripeCustomerId, CancellationToken cancellationToken = default(CancellationToken)); + + Task CancelSubscriptionAsync(string stripeCustomerId, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Core/Resgrid.Model/Services/IQueueService.cs b/Core/Resgrid.Model/Services/IQueueService.cs index d32fb11c..eefd4d66 100644 --- a/Core/Resgrid.Model/Services/IQueueService.cs +++ b/Core/Resgrid.Model/Services/IQueueService.cs @@ -69,5 +69,15 @@ public interface IQueueService /// The cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Task<QueueItem>. Task SetQueueItemCompletedAsync(int queueItemId, CancellationToken cancellationToken = default(CancellationToken)); + + Task GetPendingDeleteDepartmentQueueItemAsync(int departmentId); + + Task EnqueuePendingDeleteDepartmentAsync(int departmentId, string userId, CancellationToken cancellationToken = default(CancellationToken)); + + Task> GetAllPendingDeleteDepartmentQueueItemsAsync(); + + Task UpdateQueueItem(QueueItem item, CancellationToken cancellationToken = default(CancellationToken)); + + Task CancelPendingDepartmentDeletionRequest(int departmentId, string name, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Core/Resgrid.Model/Services/ISmsService.cs b/Core/Resgrid.Model/Services/ISmsService.cs index c009c89f..8fbdc675 100644 --- a/Core/Resgrid.Model/Services/ISmsService.cs +++ b/Core/Resgrid.Model/Services/ISmsService.cs @@ -17,7 +17,7 @@ public interface ISmsService /// The profile. /// Task<System.Boolean>. Task SendMessageAsync(Message message, string departmentNumber, int departmentId, - UserProfile profile = null); + UserProfile profile = null, Payment payment = null); /// /// Sends the call asynchronous. @@ -30,7 +30,7 @@ Task SendMessageAsync(Message message, string departmentNumber, int depart /// The address. /// Task<System.Boolean>. Task SendCallAsync(Call call, CallDispatch dispatch, string departmentNumber, int departmentId, - UserProfile profile = null, string address = null); + UserProfile profile = null, string address = null, Payment payment = null); /// /// Sends the trouble alert. diff --git a/Core/Resgrid.Model/Services/ISubscriptionsService.cs b/Core/Resgrid.Model/Services/ISubscriptionsService.cs index d00505dc..bcbe1bf7 100644 --- a/Core/Resgrid.Model/Services/ISubscriptionsService.cs +++ b/Core/Resgrid.Model/Services/ISubscriptionsService.cs @@ -224,5 +224,9 @@ Task CreateFreePlanPaymentAsync(int departmentId, string userId, Task> GetAllAddonPlansAsync(); Task SavePaymentAddonAsync(PaymentAddon paymentAddon, CancellationToken cancellationToken = default(CancellationToken)); + + bool CanPlanSendMessageSms(int planId); + + bool CanPlanSendCallSms(int planId); } } diff --git a/Core/Resgrid.Model/Services/IUsersService.cs b/Core/Resgrid.Model/Services/IUsersService.cs index 4c50bae1..ea26f669 100644 --- a/Core/Resgrid.Model/Services/IUsersService.cs +++ b/Core/Resgrid.Model/Services/IUsersService.cs @@ -30,5 +30,6 @@ public interface IUsersService Task SavePersonnelLocationAsync(PersonnelLocation personnelLocation); Task> GetLatestLocationsForDepartmentPersonnelAsync(int departmentId); Task GetPersonnelLocationByIdAsync(string id); + Task ClearOutUserLoginAsync(string userId); } } diff --git a/Core/Resgrid.Model/Services/IVoiceService.cs b/Core/Resgrid.Model/Services/IVoiceService.cs index a1cebc56..788c0f1c 100644 --- a/Core/Resgrid.Model/Services/IVoiceService.cs +++ b/Core/Resgrid.Model/Services/IVoiceService.cs @@ -1,5 +1,6 @@  +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -29,5 +30,15 @@ Task DeleteDepartmentVoiceChannelAsync(DepartmentVoiceChannel channel, Task GetVoiceChannelByIdAsync(string voiceChannelId); Task SaveOrUpdateVoiceChannelAsync(DepartmentVoiceChannel voiceChannel, int departmentId, CancellationToken cancellationToken = default(CancellationToken)); + + Task GetCurrentUtilizationForLiveKit(int departmentId); + + Task SaveDepartmentAudioAsync(DepartmentAudio departmentAudio, CancellationToken cancellationToken = default(CancellationToken)); + + Task> GetDepartmentAudiosByDepartmentIdAsync(int departmentId); + + Task GetDepartmentAudioByIdAsync(string id); + + Task DeleteDepartmentAudioAsync(DepartmentAudio audio, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Core/Resgrid.Model/TimeZones.cs b/Core/Resgrid.Model/TimeZones.cs index 4f61dec8..8ba079dc 100644 --- a/Core/Resgrid.Model/TimeZones.cs +++ b/Core/Resgrid.Model/TimeZones.cs @@ -5,15 +5,15 @@ namespace Resgrid.Model public static class TimeZones { public static Dictionary Zones = new Dictionary() - { - {"Hawaiian Standard Time", "(GMT-10:00) Hawaii"}, - {"Alaskan Standard Time", "(GMT-09:00) Alaska"}, - {"Pacific Standard Time", "(GMT-08:00) Pacific Time (US, Canada & Tijuana)"}, - {"US Mountain Standard Time", "(GMT-07:00) Arizona"}, - {"Mountain Standard Time", "(GMT-07:00) Mountain Time (US & Canada)"}, - {"Central Standard Time", "(GMT-06:00) Central Time (US & Canada)"}, - {"Eastern Standard Time", "(GMT-05:00) Eastern Time (US & Canada)"}, - {"US Eastern Standard Time", "(GMT-05:00) Indiana (East)"}, + { + {"Hawaiian Standard Time", "(GMT-10:00) Hawaii"}, + {"Alaskan Standard Time", "(GMT-09:00) Alaska"}, + {"Pacific Standard Time", "(GMT-08:00) Pacific Time (US, Canada & Tijuana)"}, + {"US Mountain Standard Time", "(GMT-07:00) Arizona"}, + {"Mountain Standard Time", "(GMT-07:00) Mountain Time (US & Canada)"}, + {"Central Standard Time", "(GMT-06:00) Central Time (US & Canada)"}, + {"Eastern Standard Time", "(GMT-05:00) Eastern Time (US & Canada)"}, + {"US Eastern Standard Time", "(GMT-05:00) Indiana (East)"}, {"Dateline Standard Time", "(GMT-12:00) International Date Line West"}, {"Samoa Standard Time", "(GMT-11:00) Midway Island & Samoa"}, {"Mountain Standard Time (Mexico)", "(GMT-07:00) Chihuahua, La Paz, Mazatlan"}, @@ -45,8 +45,8 @@ public static class TimeZones {"Egypt Standard Time", "(GMT+02:00) Cairo"}, {"South Africa Standard Time", "(UTC+02:00) Harare, Pretoria"}, {"FLE Standard Time", "(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius"}, - {"Turkey Standard Time", "(GMT+02:00) Istanbul"}, {"Israel Standard Time", "(GMT+02:00) Jerusalem"}, + {"Turkey Standard Time", "(GMT+03:00) Istanbul"}, {"Arabic Standard Time", "(GMT+03:00) Baghdad"}, {"Arab Standard Time", "(UTC+03:00) Kuwait, Riyadh"}, {"E. Europe Standard Time", "(GMT+03:00) Minsk"}, @@ -91,6 +91,6 @@ public static class TimeZones {"Fiji Standard Time", "(GMT+12:00) Fiji"}, {"Magadan Standard Time", "(GMT+12:00) Magadan"}, {"Tonga Standard Time", "(GMT+13:00) Nuku'alofa"} - }; + }; } } diff --git a/Core/Resgrid.Model/UserProfile.cs b/Core/Resgrid.Model/UserProfile.cs index 2e764fe3..3306ea62 100644 --- a/Core/Resgrid.Model/UserProfile.cs +++ b/Core/Resgrid.Model/UserProfile.cs @@ -111,6 +111,9 @@ public class UserProfile: IEntity [ProtoMember(31)] public DateTime? EndDate { get; set; } + [ProtoMember(32)] + public string Language { get; set; } + public string GetPhoneNumber() { if (!String.IsNullOrEmpty(MobileNumber)) diff --git a/Core/Resgrid.Services/AddressService.cs b/Core/Resgrid.Services/AddressService.cs index 0aa92a47..4f7f8584 100644 --- a/Core/Resgrid.Services/AddressService.cs +++ b/Core/Resgrid.Services/AddressService.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using Resgrid.Model; using Resgrid.Model.Providers; @@ -9,22 +10,42 @@ namespace Resgrid.Services { public class AddressService : IAddressService { + private static string CacheKey = "Address_{0}"; + private static TimeSpan CacheLength = TimeSpan.FromDays(90); + private readonly IAddressRepository _addressRepository; private readonly IAddressVerificationProvider _addressVerificationProvider; + private readonly ICacheProvider _cacheProvider; - public AddressService(IAddressRepository addressRepository, IAddressVerificationProvider addressVerificationProvider) + public AddressService(IAddressRepository addressRepository, IAddressVerificationProvider addressVerificationProvider, ICacheProvider cacheProvider) { _addressRepository = addressRepository; _addressVerificationProvider = addressVerificationProvider; + _cacheProvider = cacheProvider; } public async Task
GetAddressByIdAsync(int addressId) { - return await _addressRepository.GetByIdAsync(addressId); + async Task
getDepartmentGroup() + { + var address = await _addressRepository.GetByIdAsync(addressId); + + return address; + } + + if (Config.SystemBehaviorConfig.CacheEnabled) + { + return await _cacheProvider.RetrieveAsync(string.Format(CacheKey, addressId), getDepartmentGroup, CacheLength); + } + + return await getDepartmentGroup(); } public async Task
SaveAddressAsync(Address address, CancellationToken cancellationToken = default(CancellationToken)) { + if (address.AddressId > 0) + _cacheProvider.Remove(string.Format(CacheKey, address.AddressId)); + return await _addressRepository.SaveOrUpdateAsync(address, cancellationToken); } @@ -32,5 +53,15 @@ public async Task IsAddressValidAsync(Address address { return await _addressVerificationProvider.VerifyAddressAsync(address); } + + public async Task DeleteAddress(int addressId, CancellationToken cancellationToken = default(CancellationToken)) + { + var address = await _addressRepository.GetByIdAsync(addressId); + + if (address != null) + return await _addressRepository.DeleteAsync(address, cancellationToken); + + return false; + } } } diff --git a/Core/Resgrid.Services/AuthorizationService.cs b/Core/Resgrid.Services/AuthorizationService.cs index 63e8dd25..54939f05 100644 --- a/Core/Resgrid.Services/AuthorizationService.cs +++ b/Core/Resgrid.Services/AuthorizationService.cs @@ -782,7 +782,7 @@ public async Task CanUserDeleteCallAsync(string userId, int callId, int de if (call == null || call.DepartmentId != departmentId) return false; - call = await _callsService.PopulateCallData(call, false, false, false, true, false, false, false); + call = await _callsService.PopulateCallData(call, false, false, false, true, false, false, false, false); if (group != null) { @@ -821,7 +821,7 @@ public async Task CanUserCloseCallAsync(string userId, int callId, int dep if (call == null || call.DepartmentId != departmentId) return false; - call = await _callsService.PopulateCallData(call, false, false, false, true, false, false, false); + call = await _callsService.PopulateCallData(call, false, false, false, true, false, false, false, false); if (group != null) { @@ -860,7 +860,7 @@ public async Task CanUserAddCallDataAsync(string userId, int callId, int d if (call == null || call.DepartmentId != departmentId) return false; - call = await _callsService.PopulateCallData(call, false, false, false, true, false, false, false); + call = await _callsService.PopulateCallData(call, false, false, false, true, false, false, false, false); if (group != null) { @@ -872,5 +872,18 @@ public async Task CanUserAddCallDataAsync(string userId, int callId, int d return _permissionsService.IsUserAllowed(permission, departmentId, callGroupId, userGroupId, department.IsUserAnAdmin(userId), isGroupAdmin, roles); } + + public async Task CanUserDeleteDepartmentAsync(string userId, int departmentId) + { + var department = await _departmentsService.GetDepartmentByIdAsync(departmentId); + + if (department == null) + return false; + + if (department.ManagingUserId != userId) + return false; + + return true; + } } } diff --git a/Core/Resgrid.Services/CallsService.cs b/Core/Resgrid.Services/CallsService.cs index 9eb86f3a..dbaacc0f 100644 --- a/Core/Resgrid.Services/CallsService.cs +++ b/Core/Resgrid.Services/CallsService.cs @@ -39,6 +39,7 @@ public class CallsService : ICallsService private readonly ICallProtocolsRepository _callProtocolsRepository; private readonly IGeoLocationProvider _geoLocationProvider; private readonly IDepartmentsService _departmentsService; + private readonly ICallReferencesRepository _callReferencesRepository; public CallsService(ICallsRepository callsRepository, ICommunicationService communicationService, ICallDispatchesRepository callDispatchesRepository, ICallTypesRepository callTypesRepository, ICallEmailFactory callEmailFactory, @@ -46,7 +47,8 @@ public CallsService(ICallsRepository callsRepository, ICommunicationService comm ICallAttachmentRepository callAttachmentRepository, ICallDispatchGroupRepository callDispatchGroupRepository, ICallDispatchUnitRepository callDispatchUnitRepository, ICallDispatchRoleRepository callDispatchRoleRepository, IDepartmentCallPriorityRepository departmentCallPriorityRepository, IShortenUrlProvider shortenUrlProvider, - ICallProtocolsRepository callProtocolsRepository, IGeoLocationProvider geoLocationProvider, IDepartmentsService departmentsService) + ICallProtocolsRepository callProtocolsRepository, IGeoLocationProvider geoLocationProvider, IDepartmentsService departmentsService, + ICallReferencesRepository callReferencesRepository) { _callsRepository = callsRepository; _communicationService = communicationService; @@ -64,12 +66,13 @@ public CallsService(ICallsRepository callsRepository, ICommunicationService comm _callProtocolsRepository = callProtocolsRepository; _geoLocationProvider = geoLocationProvider; _departmentsService = departmentsService; + _callReferencesRepository = callReferencesRepository; } public async Task SaveCallAsync(Call call, CancellationToken cancellationToken = default(CancellationToken)) { if (String.IsNullOrWhiteSpace(call.Number)) - call.Number = await GetCurrentCallNumberAsync(call.DepartmentId); + call.Number = await GetCurrentCallNumberAsync(call.LoggedOn, call.DepartmentId); if (String.IsNullOrWhiteSpace(call.Name)) call.Name = "New Call " + DateTime.UtcNow.ToShortDateString(); @@ -113,8 +116,29 @@ public CallsService(ICallsRepository callsRepository, ICommunicationService comm dispatch.DispatchedOn = DateTime.UtcNow; } } - - return await _callsRepository.SaveOrUpdateAsync(call, cancellationToken); + + if (call.References != null && call.References.Any()) + { + foreach (var reference in call.References) + { + if (String.IsNullOrWhiteSpace(reference.CallReferenceId)) + reference.AddedOn = DateTime.UtcNow; + } + } + + var savedCall = await _callsRepository.SaveOrUpdateAsync(call, cancellationToken); + + if (call.References != null && call.References.Any()) + { + foreach (var reference in call.References) + { + reference.SourceCallId = savedCall.CallId; + + await _callReferencesRepository.SaveOrUpdateAsync(reference, cancellationToken); + } + } + + return savedCall; } public async Task RegenerateCallNumbersAsync(int departmentId, int year, CancellationToken cancellationToken = default(CancellationToken)) @@ -138,11 +162,11 @@ public CallsService(ICallsRepository callsRepository, ICommunicationService comm return true; } - public async Task GetCurrentCallNumberAsync(int departmentId) + public async Task GetCurrentCallNumberAsync(DateTime utcDate, int departmentId) { var department = await _departmentsService.GetDepartmentByIdAsync(departmentId, false); - int year = DateTimeHelpers.GetLocalDateTime(DateTime.UtcNow, department.TimeZone).Year; + int year = DateTimeHelpers.GetLocalDateTime(utcDate, department.TimeZone).Year; var start = (new DateTime(year, 1, 1, 1, 1, 1, DateTimeKind.Local)).SetToMidnight(); var end = (new DateTime(year, 12, 31, 23, 59, 59, DateTimeKind.Local)).SetToEndOfDay(); @@ -341,7 +365,12 @@ public async Task GenerateCallFromEmail(int type, CallEmail email, string public async Task GetCallAttachmentAsync(int callAttachmentId) { - return await _callAttachmentRepository.GetByIdAsync(callAttachmentId); + var attachment = await _callAttachmentRepository.GetByIdAsync(callAttachmentId); + + if (attachment != null && attachment.Call == null) + attachment.Call = await GetCallByIdAsync(attachment.CallId); + + return attachment; } public async Task SaveCallAttachmentAsync(CallAttachment attachment, CancellationToken cancellationToken = default(CancellationToken)) @@ -414,7 +443,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) + public async Task PopulateCallData(Call call, bool getDispatches, bool getAttachments, bool getNotes, bool getGroupDispatches, bool getUnitDispatches, bool getRoleDispatches, bool getProtocols, bool getReferences) { if (getDispatches && call.Dispatches == null) { @@ -480,6 +509,15 @@ public async Task PopulateCallData(Call call, bool getDispatches, bool get else call.Protocols = new List(); } + if (getReferences && call.References == null) + { + var items = await _callReferencesRepository.GetCallReferencesBySourceCallIdAsync(call.CallId); + + if (items != null) + call.References = items.ToList(); + else + call.References = new List(); + } return call; } @@ -759,6 +797,21 @@ public async Task> GetAllNonDispatchedScheduledCallsByDepartmentIdAsy return new List(); } + public async Task> GetChildCallsForCallAsync(int callId) + { + var calls = await _callReferencesRepository.GetCallReferencesByTargetCallIdAsync(callId); + + if (calls != null && calls.Any()) + return calls.ToList(); + + return new List(); + } + + public async Task DeleteCallReferenceAsync(CallReference callReference, CancellationToken cancellationToken = default(CancellationToken)) + { + return await _callReferencesRepository.DeleteAsync(callReference, cancellationToken); + } + public string CallStateToString(CallStates state) { switch (state) diff --git a/Core/Resgrid.Services/CommunicationService.cs b/Core/Resgrid.Services/CommunicationService.cs index 6a99cf79..a8aab0f0 100644 --- a/Core/Resgrid.Services/CommunicationService.cs +++ b/Core/Resgrid.Services/CommunicationService.cs @@ -21,9 +21,12 @@ public class CommunicationService : ICommunicationService private readonly IOutboundVoiceProvider _outboundVoiceProvider; private readonly IUserProfileService _userProfileService; private readonly IDepartmentSettingsService _departmentSettingsService; + private readonly ISubscriptionsService _subscriptionsService; + private readonly IUserStateService _userStateService; public CommunicationService(ISmsService smsService, IEmailService emailService, IPushService pushService, IGeoLocationProvider geoLocationProvider, - IOutboundVoiceProvider outboundVoiceProvider, IUserProfileService userProfileService, IDepartmentSettingsService departmentSettingsService) + IOutboundVoiceProvider outboundVoiceProvider, IUserProfileService userProfileService, IDepartmentSettingsService departmentSettingsService, + ISubscriptionsService subscriptionsService, IUserStateService userStateService) { _smsService = smsService; _emailService = emailService; @@ -32,13 +35,18 @@ public CommunicationService(ISmsService smsService, IEmailService emailService, _outboundVoiceProvider = outboundVoiceProvider; _userProfileService = userProfileService; _departmentSettingsService = departmentSettingsService; + _subscriptionsService = subscriptionsService; + _userStateService = userStateService; } public async Task SendMessageAsync(Message message, string sendersName, string departmentNumber, int departmentId, UserProfile profile = null) { if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId)) return false; - + + if (!await CanSendToUser(message.ReceivingUserId, departmentId)) + return false; + if (profile == null && !String.IsNullOrWhiteSpace(message.ReceivingUserId)) profile = await _userProfileService.GetProfileByUserIdAsync(message.ReceivingUserId); @@ -46,7 +54,8 @@ public async Task SendMessageAsync(Message message, string sendersName, st { try { - await _smsService.SendMessageAsync(message, departmentNumber, departmentId, profile); + var payment = await _subscriptionsService.GetCurrentPaymentForDepartmentAsync(departmentId); + await _smsService.SendMessageAsync(message, departmentNumber, departmentId, profile, payment); } catch (Exception ex) { @@ -95,6 +104,12 @@ public async Task SendMessageAsync(Message message, string sendersName, st public async Task SendCallAsync(Call call, CallDispatch dispatch, string departmentNumber, int departmentId, UserProfile profile = null, string address = null) { + if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId)) + return false; + + if (!await CanSendToUser(dispatch.UserId, departmentId)) + return false; + if (profile == null) profile = await _userProfileService.GetProfileByUserIdAsync(dispatch.UserId); @@ -176,7 +191,8 @@ public async Task SendCallAsync(Call call, CallDispatch dispatch, string d // Send an SMS Message if (profile == null || profile.SendSms) { - await _smsService.SendCallAsync(call, dispatch, departmentNumber, departmentId, profile); + var payment = await _subscriptionsService.GetCurrentPaymentForDepartmentAsync(departmentId); + await _smsService.SendCallAsync(call, dispatch, departmentNumber, departmentId, profile, call.Address, payment); } // Send an Email @@ -277,7 +293,10 @@ public async Task SendNotificationAsync(string userId, int departmentId, s { if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId)) return false; - + + if (!await CanSendToUser(userId, departmentId)) + return false; + if (profile == null) profile = await _userProfileService.GetProfileByUserIdAsync(userId, false); @@ -319,6 +338,9 @@ public async Task SendCalendarAsync(string userId, int departmentId, strin if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId)) return false; + if (!await CanSendToUser(userId, departmentId)) + return false; + if (profile == null) profile = await _userProfileService.GetProfileByUserIdAsync(userId, false); @@ -355,11 +377,10 @@ public async Task SendCalendarAsync(string userId, int departmentId, strin return true; } - public async Task SendChat(string chatId, string sendingUserId, string group, string message, UserProfile sendingUser, List recipients) + public async Task SendChat(string chatId, int departmentId, string sendingUserId, string group, string message, UserProfile sendingUser, List recipients) { var spm = new StandardPushMessage(); - if (recipients.Count == 1) { spm.Title = $"New Chat Message from {sendingUser.FullName.AsFirstNameLastName}"; @@ -378,6 +399,10 @@ public async Task SendChat(string chatId, string sendingUserId, string gro var sendingTo = recipients.FirstOrDefault(); spm.Id = $"T{sendingTo}"; + + if (!await CanSendToUser(sendingTo.UserId, departmentId)) + return false; + if (sendingTo != null) { await _pushService.PushChat(spm, sendingTo.UserId, sendingTo); @@ -388,16 +413,19 @@ public async Task SendChat(string chatId, string sendingUserId, string gro spm.Id = $"G{chatId}"; //await recipients.ParallelForEachAsync(async person => foreach (var person in recipients) + { + try { - try + if (await CanSendToUser(person.UserId, departmentId)) { await _pushService.PushChat(spm, person.UserId, person); } - catch (Exception ex) - { - Logging.LogException(ex); - } } + catch (Exception ex) + { + Logging.LogException(ex); + } + } } } catch (Exception ex) @@ -491,5 +519,22 @@ public async Task SendTextMessageAsync(string userId, string title, string { return await _smsService.SendTextAsync(userId, title, message, departmentId, departmentNumber, profile); } + + private async Task CanSendToUser(string userId, int departmentId) + { + var supressStaffingInfo = await _departmentSettingsService.GetDepartmentStaffingSuppressInfoAsync(departmentId); + var lastUserStaffing = await _userStateService.GetLastUserStateByUserIdAsync(userId); + + if (lastUserStaffing == null && supressStaffingInfo != null) + { + if (supressStaffingInfo.EnableSupressStaffing) + { + if (supressStaffingInfo.StaffingLevelsToSupress.Contains(lastUserStaffing.State)) + return false; + } + } + + return true; + } } } diff --git a/Core/Resgrid.Services/CustomStateService.cs b/Core/Resgrid.Services/CustomStateService.cs index 43a64b78..0f989b92 100644 --- a/Core/Resgrid.Services/CustomStateService.cs +++ b/Core/Resgrid.Services/CustomStateService.cs @@ -302,15 +302,15 @@ public async Task GetCustomUnitStateAsync(UnitState state) public List GetDefaultUnitStatuses() { List details = new List(); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Responding, ButtonText = "Responding", ButtonColor = "#32db64", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Calls }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Available, ButtonText = "Available", ButtonColor = "#222222", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Stations }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Responding, ButtonText = "Responding", ButtonColor = "#32db64", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Calls }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Available, ButtonText = "Available", ButtonColor = "#d1dade", TextColor = "5E5E5E", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Stations }); //details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Unavailable, ButtonText = "Unavailable", ButtonColor = "" }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Committed, ButtonText = "Committed", ButtonColor = "#50b8de", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.CallsAndStations }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Committed, ButtonText = "Committed", ButtonColor = "#50b8de", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.CallsAndStations }); //details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Delayed, ButtonText = "Delayed", ButtonColor = "" }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.OnScene, ButtonText = "On Scene", ButtonColor = "#69BB7B", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Calls }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Staging, ButtonText = "Staging", ButtonColor = "#ffc900", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Calls }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Returning, ButtonText = "Returning", ButtonColor = "#387ef5", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Stations }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.OutOfService, ButtonText = "Out of Service", ButtonColor = "#ff6b69", NoteType = (int)CustomStateNoteTypes.Optional }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.OnScene, ButtonText = "On Scene", ButtonColor = "#69BB7B", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Calls }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Staging, ButtonText = "Staging", ButtonColor = "#ffc900", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Calls }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Returning, ButtonText = "Returning", ButtonColor = "#387ef5", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Stations }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.OutOfService, ButtonText = "Out of Service", ButtonColor = "#ff6b69", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional }); //details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Cancelled, ButtonText = "Cancelled", ButtonColor = "#ff6b69" }); //details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Released, ButtonText = "Released", ButtonColor = "" }); //details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UnitStateTypes.Manual, ButtonText = "Manual", ButtonColor = "" }); @@ -323,13 +323,13 @@ public List GetDefaultUnitStatuses() public List GetDefaultPersonStatuses() { List details = new List(); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.Responding, ButtonText = "Responding", ButtonColor = "#449d44", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.CallsAndStations }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.NotResponding, ButtonText = "Not Responding", ButtonColor = "#ed5565", NoteType = (int)CustomStateNoteTypes.Optional }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.OnScene, ButtonText = "On Scene", ButtonColor = "#262626", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Calls }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.StandingBy, ButtonText = "Standing By", ButtonColor = "#d1dade", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Stations }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.AvailableStation, ButtonText = "Available Station", ButtonColor = "#d1dade", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Stations }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.RespondingToStation, ButtonText = "Responding to Station", ButtonColor = "#449d44", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Stations }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.RespondingToScene, ButtonText = "Responding to Scene", ButtonColor = "#449d44", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Calls }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.Responding, ButtonText = "Responding", ButtonColor = "#449d44", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.CallsAndStations }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.NotResponding, ButtonText = "Not Responding", ButtonColor = "#ed5565", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.OnScene, ButtonText = "On Scene", ButtonColor = "#262626", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Calls }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.StandingBy, ButtonText = "Standing By", ButtonColor = "#d1dade", TextColor = "5E5E5E", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Stations }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.AvailableStation, ButtonText = "Available Station", ButtonColor = "#d1dade", TextColor = "5E5E5E", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Stations }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.RespondingToStation, ButtonText = "Responding to Station", ButtonColor = "#449d44", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Stations }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)ActionTypes.RespondingToScene, ButtonText = "Responding to Scene", ButtonColor = "#449d44", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional, DetailType = (int)CustomStateDetailTypes.Calls }); return details; } @@ -337,11 +337,11 @@ public List GetDefaultPersonStatuses() public List GetDefaultPersonStaffings() { List details = new List(); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UserStateTypes.Available, ButtonText = "Available", ButtonColor = "#d1dade", NoteType = (int)CustomStateNoteTypes.Optional }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UserStateTypes.Delayed, ButtonText = "Delayed", ButtonColor = "#f8ac59", NoteType = (int)CustomStateNoteTypes.Optional }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UserStateTypes.Unavailable, ButtonText = "Unavailable", ButtonColor = "#ed5565", NoteType = (int)CustomStateNoteTypes.Optional }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UserStateTypes.Committed, ButtonText = "Committed", ButtonColor = "#23c6c8", NoteType = (int)CustomStateNoteTypes.Optional }); - details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UserStateTypes.OnShift, ButtonText = "On Shift", ButtonColor = "#228bcb", NoteType = (int)CustomStateNoteTypes.Optional }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UserStateTypes.Available, ButtonText = "Available", ButtonColor = "#d1dade", TextColor = "5E5E5E", NoteType = (int)CustomStateNoteTypes.Optional }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UserStateTypes.Delayed, ButtonText = "Delayed", ButtonColor = "#f8ac59", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UserStateTypes.Unavailable, ButtonText = "Unavailable", ButtonColor = "#ed5565", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UserStateTypes.Committed, ButtonText = "Committed", ButtonColor = "#23c6c8", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional }); + details.Add(new CustomStateDetail() { CustomStateDetailId = (int)UserStateTypes.OnShift, ButtonText = "On Shift", ButtonColor = "#228bcb", TextColor = "#ffffff", NoteType = (int)CustomStateNoteTypes.Optional }); return details; diff --git a/Core/Resgrid.Services/DeleteService.cs b/Core/Resgrid.Services/DeleteService.cs index 7f485148..a1346c96 100644 --- a/Core/Resgrid.Services/DeleteService.cs +++ b/Core/Resgrid.Services/DeleteService.cs @@ -1,8 +1,13 @@ using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Transactions; +using Resgrid.Framework; using Resgrid.Model; +using Resgrid.Model.Events; +using Resgrid.Model.Providers; +using Resgrid.Model.Repositories; using Resgrid.Model.Services; namespace Resgrid.Services @@ -26,12 +31,20 @@ public class DeleteService : IDeleteService private readonly ICertificationService _certificationService; private readonly ILogService _logService; private readonly IInventoryService _inventoryService; + private readonly IEventAggregator _eventAggregator; + private readonly IAddressService _addressService; + private readonly IQueueService _queueService; + private readonly IEmailService _emailService; + private readonly IDeleteRepository _deleteRepository; public DeleteService(IAuthorizationService authorizationService, IDepartmentsService departmentsService, ICallsService callsService, IActionLogsService actionLogsService, IUsersService usersService, IUserProfileService userProfileService, IMessageService messageService, IDepartmentGroupsService departmentGroupsService, - IWorkLogsService workLogsService, IUserStateService userStateService, IPersonnelRolesService personnelRolesService, IDistributionListsService distributionListsService, - IShiftsService shiftsService, IUnitsService unitsService, ICertificationService certificationService, ILogService logService, IInventoryService inventoryService) + IWorkLogsService workLogsService, IUserStateService userStateService, IPersonnelRolesService personnelRolesService, + IDistributionListsService distributionListsService, IShiftsService shiftsService, IUnitsService unitsService, + ICertificationService certificationService, ILogService logService, IInventoryService inventoryService, + IEventAggregator eventAggregator, IAddressService addressService, IQueueService queueService, IEmailService emailService, + IDeleteRepository deleteRepository) { _authorizationService = authorizationService; _departmentsService = departmentsService; @@ -50,6 +63,11 @@ public DeleteService(IAuthorizationService authorizationService, IDepartmentsSer _certificationService = certificationService; _logService = logService; _inventoryService = inventoryService; + _eventAggregator = eventAggregator; + _addressService = addressService; + _queueService = queueService; + _emailService = emailService; + _deleteRepository = deleteRepository; } public async Task DeleteUserAsync(int departmentId, string authorizingUserId, string userIdToDelete) @@ -83,10 +101,89 @@ public async Task DeleteUserAsync(int departmentId, string au return DeleteUserResults.NoFailure; } + public async Task DeleteUserAccountAsync(int departmentId, string authorizingUserId, string userIdToDelete, string ipAddress, string userAgent, CancellationToken cancellationToken = default(CancellationToken)) + { + //if (!await _authorizationService.CanUserDeleteUserAsync(departmentId, authorizingUserId, userIdToDelete)) + // return DeleteUserResults.UnAuthroized; + + if (authorizingUserId != userIdToDelete) + return DeleteUserResults.UnAuthroized; + + var departments = await _departmentsService.GetAllDepartmentsForUserAsync(userIdToDelete); + + if (departments != null && departments.Any()) + { + foreach (var dm in departments) + { + var dep = await _departmentsService.GetDepartmentByUserIdAsync(userIdToDelete); + + if (dep.ManagingUserId == userIdToDelete) + return DeleteUserResults.UserIsManagingDepartmentAdmin; + + + var auditEvent = new AuditEvent(); + auditEvent.Before = dm.CloneJsonToString(); + auditEvent.DepartmentId = dm.DepartmentId; + auditEvent.UserId = userIdToDelete; + auditEvent.Type = AuditLogTypes.UserAccountDeleted; + + dm.IsDeleted = true; + dm.IsAdmin = false; + dm.IsHidden = true; + dm.IsDefault = false; + dm.IsActive = false; + dm.IsDisabled = true; + + auditEvent.After = dm.CloneJsonToString(); + auditEvent.Successful = true; + auditEvent.IpAddress = ipAddress; + auditEvent.ServerName = Environment.MachineName; + auditEvent.UserAgent = userAgent; + _eventAggregator.SendMessage(auditEvent); + + await _departmentsService.SaveDepartmentMemberAsync(dm, cancellationToken); + } + } + + var userProfile = await _userProfileService.GetProfileByUserIdAsync(userIdToDelete, true); + + if (userProfile != null) + { + userProfile.MobileCarrier = 0; + userProfile.MobileNumber = null; + userProfile.SendPush = false; + userProfile.SendEmail = false; + userProfile.SendSms = false; + userProfile.SendMessageEmail = false; + userProfile.SendMessagePush = false; + userProfile.SendMessageSms = false; + userProfile.SendNotificationEmail = false; + userProfile.SendNotificationPush = false; + userProfile.SendNotificationSms = false; + userProfile.DoNotRecieveNewsletters = true; + userProfile.HomeNumber = null; + userProfile.VoiceForCall = false; + userProfile.VoiceCallHome = false; + userProfile.VoiceCallMobile = false; + + if (userProfile.HomeAddressId.HasValue) + await _addressService.DeleteAddress(userProfile.HomeAddressId.Value, cancellationToken); + + if (userProfile.MailingAddressId.HasValue) + await _addressService.DeleteAddress(userProfile.MailingAddressId.Value, cancellationToken); + + await _userProfileService.SaveProfileAsync(departmentId, userProfile, cancellationToken); + } + + await _usersService.ClearOutUserLoginAsync(userIdToDelete); + + return DeleteUserResults.NoFailure; + } + public async Task DeleteGroupAsync(int departmentGroupId, int departmentId, string currentUserId, CancellationToken cancellationToken = default(CancellationToken)) { if (!await _authorizationService.CanUserEditDepartmentGroupAsync(currentUserId, departmentGroupId)) - return DeleteGroupResults.UnAuthroized; + return DeleteGroupResults.UnAuthorized; await _callsService.ClearGroupForDispatchesAsync(departmentGroupId, cancellationToken); await _workLogsService.ClearGroupForLogsAsync(departmentGroupId, cancellationToken); @@ -98,5 +195,91 @@ public async Task DeleteUserAsync(int departmentId, string au return DeleteGroupResults.NoFailure; } + + public async Task DeleteDepartment(int departmentId, string authorizingUserId, string ipAddress, string userAgent, CancellationToken cancellationToken = default(CancellationToken)) + { + if (!await _authorizationService.CanUserDeleteDepartmentAsync(authorizingUserId, departmentId)) + return DeleteDepartmentResults.UnAuthorized; + + var auditEvent = new AuditEvent(); + auditEvent.Before = null; + auditEvent.DepartmentId = departmentId; + auditEvent.UserId = authorizingUserId; + auditEvent.Type = AuditLogTypes.DeleteDepartmentRequested; + auditEvent.After = null; + auditEvent.Successful = true; + auditEvent.IpAddress = ipAddress; + auditEvent.ServerName = Environment.MachineName; + auditEvent.UserAgent = userAgent; + _eventAggregator.SendMessage(auditEvent); + + var result = await _queueService.EnqueuePendingDeleteDepartmentAsync(departmentId, authorizingUserId, cancellationToken); + + if (result != null) + { + var department = await _departmentsService.GetDepartmentByIdAsync(departmentId); + + var ownerUserProfile = await _userProfileService.GetProfileByUserIdAsync(authorizingUserId); + var result2 = await _emailService.SendDeleteDepartmentEmail(ownerUserProfile.User.Email, ownerUserProfile.FullName.AsFirstNameLastName, result); + + foreach (var adminUser in department.AdminUsers) + { + var adminUserProfile = await _userProfileService.GetProfileByUserIdAsync(adminUser); + var result1 = await _emailService.SendDeleteDepartmentEmail(adminUserProfile.User.Email, adminUserProfile.FullName.AsFirstNameLastName, result); + } + } + + return DeleteDepartmentResults.NoFailure; + } + + public async Task HandlePendingDepartmentDeletionRequestAsync(QueueItem item, CancellationToken cancellationToken = default(CancellationToken)) + { + if (!await _authorizationService.CanUserDeleteDepartmentAsync(item.QueuedByUserId, int.Parse(item.SourceId))) + return DeleteDepartmentResults.UnAuthorized; + + if (item.ToBeCompletedOn.HasValue && DateTime.UtcNow >= item.ToBeCompletedOn.Value.AddDays(-10) && item.ReminderCount == 0) + { + /* + * You have a pending department deletion request, it is within 10 days out and we have no yet sent a reminder. + */ + + var department = await _departmentsService.GetDepartmentByIdAsync(int.Parse(item.SourceId)); + + var ownerUserProfile = await _userProfileService.GetProfileByUserIdAsync(item.QueuedByUserId); + var result2 = await _emailService.SendDeleteDepartmentEmail(ownerUserProfile.User.Email, ownerUserProfile.FullName.AsFirstNameLastName, item); + + foreach (var adminUser in department.AdminUsers) + { + var adminUserProfile = await _userProfileService.GetProfileByUserIdAsync(adminUser); + var result1 = await _emailService.SendDeleteDepartmentEmail(adminUserProfile.User.Email, adminUserProfile.FullName.AsFirstNameLastName, item); + } + + item.ReminderCount += 1; + var result = await _queueService.UpdateQueueItem(item, cancellationToken); + } + else if (item.ToBeCompletedOn.HasValue && DateTime.UtcNow >= item.ToBeCompletedOn.Value) + { + /* + * You have a pending department deletion request and it can be executed now. + */ + + try + { + var result = await _deleteRepository.DeleteDepartmentAndUsersAsync(int.Parse(item.SourceId)); + + item.CompletedOn = DateTime.UtcNow; + var result2 = await _queueService.UpdateQueueItem(item, cancellationToken); + } + catch (Exception e) + { + Logging.LogException(e); + Logging.SendExceptionEmail(e, "DeleteDepartment", int.Parse(item.SourceId)); + + return DeleteDepartmentResults.Failure; + } + } + + return DeleteDepartmentResults.NoFailure; + } } } diff --git a/Core/Resgrid.Services/DepartmentGroupsService.cs b/Core/Resgrid.Services/DepartmentGroupsService.cs index 7a9fa147..1b128d8e 100644 --- a/Core/Resgrid.Services/DepartmentGroupsService.cs +++ b/Core/Resgrid.Services/DepartmentGroupsService.cs @@ -98,7 +98,7 @@ public async Task> GetAllGroupsForDepartmentAsync(int depa { if (group.ParentDepartmentGroupId.HasValue) { - group.Parent = await GetGroupByIdAsync(group.ParentDepartmentGroupId.Value); + group.Parent = await GetGroupByIdAsync(group.ParentDepartmentGroupId.Value, false); } var childGroups = await _departmentGroupsRepository.GetAllGroupsByParentGroupIdAsync(group.DepartmentGroupId); @@ -128,7 +128,7 @@ public async Task> GetAllGroupsForDepartmentUnlimitedAsync if (g.ParentDepartmentGroupId.HasValue) { - g.Parent = await GetGroupByIdAsync(g.ParentDepartmentGroupId.Value); + g.Parent = await GetGroupByIdAsync(g.ParentDepartmentGroupId.Value, false); } var childGroups = await _departmentGroupsRepository.GetAllGroupsByParentGroupIdAsync(g.DepartmentGroupId); @@ -142,6 +142,13 @@ public async Task> GetAllGroupsForDepartmentUnlimitedAsync return groups.ToList(); } + public async Task> GetAllGroupsForDepartmentUnlimitedThinAsync(int departmentId) + { + var groups = await _departmentGroupsRepository.GetAllGroupsByDepartmentIdAsync(departmentId); + + return groups.ToList(); + } + public async Task GetGroupByIdAsync(int departmentGroupId, bool bypassCache = true) { async Task getDepartmentGroup() diff --git a/Core/Resgrid.Services/DepartmentSettingsService.cs b/Core/Resgrid.Services/DepartmentSettingsService.cs index ccfc9545..99420e3f 100644 --- a/Core/Resgrid.Services/DepartmentSettingsService.cs +++ b/Core/Resgrid.Services/DepartmentSettingsService.cs @@ -15,7 +15,10 @@ public class DepartmentSettingsService : IDepartmentSettingsService { private static string DisableAutoAvailableCacheKey = "DSetAutoAvailable_{0}"; private static string StripeCustomerCacheKey = "DSetStripeCus_{0}"; + private static string BigBoardCenterGps = "DSetBBCenterGps_{0}"; + private static string StaffingSupressInfo = "DSetStaffingSupress_{0}"; private static TimeSpan LongCacheLength = TimeSpan.FromDays(14); + private static TimeSpan ThatsNotLongThisIsLongCacheLength = TimeSpan.FromDays(365); private static TimeSpan TwoYearCacheLength = TimeSpan.FromDays(730); private readonly IDepartmentSettingsRepository _departmentSettingsRepository; @@ -47,6 +50,20 @@ public DepartmentSettingsService(IDepartmentSettingsRepository departmentSetting } else { + // Clear out Cache + switch (type) + { + case DepartmentSettingTypes.BigBoardMapCenterGpsCoordinates: + _cacheProvider.Remove(string.Format(BigBoardCenterGps, departmentId)); + break; + case DepartmentSettingTypes.DisabledAutoAvailable: + _cacheProvider.Remove(string.Format(DisableAutoAvailableCacheKey, departmentId)); + break; + case DepartmentSettingTypes.StaffingSuppressStaffingLevels: + _cacheProvider.Remove(string.Format(StaffingSupressInfo, departmentId)); + break; + } + savedSetting.Setting = setting; return await _departmentSettingsRepository.SaveOrUpdateAsync(savedSetting, cancellationToken); } @@ -96,56 +113,71 @@ public async Task
GetBigBoardCenterAddressDepartmentAsync(int departmen public async Task GetBigBoardCenterGpsCoordinatesDepartmentAsync(int departmentId) { - var center = await GetSettingByDepartmentIdType(departmentId, DepartmentSettingTypes.BigBoardMapCenterGpsCoordinates); + string location; - if (center != null) + async Task getLocation() { - var newLocation = String.Empty; - var points = center.Setting.Split(char.Parse(",")); + var center = await GetSettingByDepartmentIdType(departmentId, DepartmentSettingTypes.BigBoardMapCenterGpsCoordinates); - try + if (center != null) { - if (points.Length == 2) + var newLocation = String.Empty; + var points = center.Setting.Split(char.Parse(",")); + + try { - if (!String.IsNullOrWhiteSpace(points[0])) + if (points.Length == 2) { - if (Framework.LocationHelpers.IsDMSLocation(points[0])) + if (!String.IsNullOrWhiteSpace(points[0])) { - newLocation = Framework.LocationHelpers.ConvertDegreeAngleToDouble(points[0]).ToString() + ","; + if (Framework.LocationHelpers.IsDMSLocation(points[0])) + { + newLocation = Framework.LocationHelpers.ConvertDegreeAngleToDouble(points[0]).ToString() + ","; + } + else + { + newLocation = LocationHelpers.StripNonDecimalCharacters(points[0]) + ","; + } } - else + + if (!String.IsNullOrWhiteSpace(points[1])) { - newLocation = LocationHelpers.StripNonDecimalCharacters(points[0]) + ","; + if (Framework.LocationHelpers.IsDMSLocation(points[1])) + { + newLocation = newLocation + Framework.LocationHelpers.ConvertDegreeAngleToDouble(points[1]).ToString(); + } + else + { + newLocation = newLocation + LocationHelpers.StripNonDecimalCharacters(points[1]); + } } - } - if (!String.IsNullOrWhiteSpace(points[1])) + } + else { - if (Framework.LocationHelpers.IsDMSLocation(points[1])) - { - newLocation = newLocation + Framework.LocationHelpers.ConvertDegreeAngleToDouble(points[1]).ToString(); - } - else - { - newLocation = newLocation + LocationHelpers.StripNonDecimalCharacters(points[1]); - } + newLocation = center.Setting; } - } - else + catch (Exception ex) { - newLocation = center.Setting; + newLocation = "0,0"; } - } - catch (Exception ex) - { - newLocation = "0,0"; + + return newLocation; } - return newLocation; + return null; } - return null; + if (Config.SystemBehaviorConfig.CacheEnabled) + { + return await _cacheProvider.RetrieveAsync(string.Format(BigBoardCenterGps, departmentId), + getLocation, TwoYearCacheLength); + } + else + { + return await getLocation(); + } } public async Task GetBigBoardHideUnavailableDepartmentAsync(int departmentId) @@ -560,5 +592,35 @@ private async Task GetSettingBySettingTypeAsync(string settin { return await _departmentSettingsRepository.GetDepartmentSettingBySettingTypeAsync(setting, settingType); } + + public async Task GetDepartmentStaffingSuppressInfoAsync(int departmentId, bool bypassCache = false) + { + async Task getSetting() + { + var actualSetting = await GetSettingByDepartmentIdType(departmentId, DepartmentSettingTypes.StaffingSuppressStaffingLevels); + + if (actualSetting != null) + { + var setting = ObjectSerialization.Deserialize(actualSetting.Setting); + + if (setting != null) + return setting; + else + return new DepartmentSuppressStaffingInfo(); + } + + return new DepartmentSuppressStaffingInfo(); + } + + if (!bypassCache && Config.SystemBehaviorConfig.CacheEnabled) + { + var cachedValue = await _cacheProvider.RetrieveAsync(string.Format(StaffingSupressInfo, departmentId), + getSetting, ThatsNotLongThisIsLongCacheLength); + + return cachedValue; + } + + return await getSetting(); + } } } diff --git a/Core/Resgrid.Services/DepartmentsService.cs b/Core/Resgrid.Services/DepartmentsService.cs index 92c011bf..1740936e 100644 --- a/Core/Resgrid.Services/DepartmentsService.cs +++ b/Core/Resgrid.Services/DepartmentsService.cs @@ -593,6 +593,7 @@ async Task getDepartmentMember() InvalidateDepartmentMemberInCache(departmentMember.UserId, departmentMember.DepartmentId); InvalidateDepartmentUserInCache(departmentMember.UserId, departmentMember.User); + InvalidateAllDepartmentsCache(departmentMember.DepartmentId); return saved; } diff --git a/Core/Resgrid.Services/EmailService.cs b/Core/Resgrid.Services/EmailService.cs index fc6ed67e..6a4efc1e 100644 --- a/Core/Resgrid.Services/EmailService.cs +++ b/Core/Resgrid.Services/EmailService.cs @@ -32,7 +32,7 @@ public class EmailService : IEmailService private SmtpClient _smtpClient; - public EmailService(IUserProfileService userProfileService, IUsersService usersService, IGeoLocationProvider geoLocationProvider, IEmailProvider emailProvider, + public EmailService(IUserProfileService userProfileService, IUsersService usersService, IGeoLocationProvider geoLocationProvider, IEmailProvider emailProvider, IDepartmentsService departmentsService, ICallEmailProvider callEmailProvider, IEmailSender emailSender, IAmazonEmailSender amazonEmailSender) { _userProfileService = userProfileService; @@ -136,7 +136,7 @@ public async Task SendMessageAsync(Message message, string senderName, int { if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId)) return false; - + if (profile == null && !String.IsNullOrWhiteSpace(message.ReceivingUserId)) profile = await _userProfileService.GetProfileByUserIdAsync(message.ReceivingUserId); @@ -301,7 +301,7 @@ public async Task SendInviteAsync(Invite invite, string senderName, string { if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(invite.DepartmentId)) return false; - + if (invite == null) return false; @@ -314,7 +314,7 @@ public async Task SendInviteAsync(Invite invite, string senderName, string } public async Task SendDistributionListEmail(MimeMessage message, string emailAddress, string name, string listUsername, string listEmail) - { + { // VERP https://www.limilabs.com/blog/verp-variable-envelope-return-path-net message.From.Clear(); @@ -334,7 +334,7 @@ public async Task SendPaymentReceiptEmailAsync(Payment payment, Department { if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(department.DepartmentId)) return false; - + IdentityUser user; if (department.ManagingUser != null) @@ -355,7 +355,7 @@ public async Task SendCancellationEmailAsync(Payment payment, Department d { if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(department.DepartmentId)) return false; - + if (department == null && payment != null && payment.Department != null) department = payment.Department; @@ -381,7 +381,7 @@ public async Task SendChargeFailedEmailAsync(Payment payment, Department d { if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(department.DepartmentId)) return false; - + if (payment != null && department != null) { var user = _usersService.GetUserById(department.ManagingUserId, false); @@ -404,7 +404,7 @@ public async Task SendCancellationWithRefundEmailAsync(Payment payment, Ch { if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(department.DepartmentId)) return false; - + var user = _usersService.GetUserById(department.ManagingUserId, false); var profile = await _userProfileService.GetProfileByUserIdAsync(user.UserId); @@ -418,7 +418,7 @@ public async Task SendUserCancellationNotificationToTeamAsync(Department d { if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(department.DepartmentId)) return false; - + var user = _usersService.GetUserById(department.ManagingUserId, false); var profile = await _userProfileService.GetProfileByUserIdAsync(user.UserId); @@ -438,7 +438,7 @@ public async Task SendRefundIssuedNotificationToTeam(Payment payment, Char { if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(department.DepartmentId)) return false; - + await _emailProvider.TEAM_SendNotifyRefundIssued(department.DepartmentId.ToString(), department.Name, DateTime.UtcNow.ToShortDateString() + " " + DateTime.UtcNow.ToShortTimeString(), (float.Parse(charge.AmountRefunded.ToString()) / 100f).ToString("C", Cultures.UnitedStates), ((PaymentMethods)payment.Method).ToString(), charge.Id, payment.PaymentId.ToString()); @@ -579,5 +579,22 @@ public async Task SendNewDepartmentLinkMailAsync(DepartmentLink link) return false; } + + public async Task SendDeleteDepartmentEmail(string sendingToEmail, string sendingToName, QueueItem queueItem) + { + if (queueItem == null) + return false; + + if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(int.Parse(queueItem.SourceId))) + return false; + + var department = await _departmentsService.GetDepartmentByIdAsync(int.Parse(queueItem.SourceId)); + var userProfile = await _userProfileService.GetProfileByUserIdAsync(queueItem.QueuedByUserId); + + if (queueItem.ToBeCompletedOn.HasValue) + return await _emailProvider.SendDeleteDepartmentEmail(userProfile.FullName.AsFirstNameLastName, department.Name, queueItem.ToBeCompletedOn.Value.TimeConverter(department), sendingToName, sendingToEmail); + + return false; + } } } diff --git a/Core/Resgrid.Services/Facades/Stripe/StripeSubscriptionServiceFacade.cs b/Core/Resgrid.Services/Facades/Stripe/StripeSubscriptionServiceFacade.cs index f1a3f28f..4a101471 100644 --- a/Core/Resgrid.Services/Facades/Stripe/StripeSubscriptionServiceFacade.cs +++ b/Core/Resgrid.Services/Facades/Stripe/StripeSubscriptionServiceFacade.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Resgrid.Model; using Resgrid.Model.Facades.Stripe; @@ -113,5 +114,20 @@ public async Task GetCurrentCanceledSubWithAddonAsync(string custo { return null; } + + public Task CancelAsync(string customerId, string subscriptionId, bool cancelAtPeriodEnd = false, CancellationToken cancellationToken = default) + { + return null; + } + + public Task AdjustPTTAddonQuantityToSubscription(Subscription sub, long quantity, PlanAddon addon, CancellationToken cancellationToken = default) + { + return null; + } + + public Task CreatePTTAddonSubscription(string customerId, long quantity, PlanAddon addon, CancellationToken cancellationToken = default) + { + return null; + } } } diff --git a/Core/Resgrid.Services/PaymentProviderService.cs b/Core/Resgrid.Services/PaymentProviderService.cs index cde9294e..cb11dbad 100644 --- a/Core/Resgrid.Services/PaymentProviderService.cs +++ b/Core/Resgrid.Services/PaymentProviderService.cs @@ -91,5 +91,50 @@ public async Task ProcessStripeInvoicePaidAsync(Invoice invoice) { return null; } + + public Task ProcessStripeSubscriptionRefundAsync(Charge stripeCharge, CancellationToken cancellationToken = default) + { + return null; + } + + public Task CreateStripeSessionForSub(int departmentId, string stripeCustomerId, string stripePlanId, int planId, string emailAddress, string departmentName, CancellationToken cancellationToken = default) + { + return null; + } + + public Task CreateStripeSessionForUpdate(int departmentId, string stripeCustomerId, string emailAddress, string departmentName, CancellationToken cancellationToken = default) + { + return null; + } + + public Task GetActiveStripeSubscriptionAsync(string stripeCustomerId, CancellationToken cancellationToken = default) + { + return null; + } + + public Task ChangeActiveSubscriptionAsync(string stripeCustomerId, string stripePlanId, CancellationToken cancellationToken = default) + { + return null; + } + + public Task ModifyPTTAddonSubscription(string stripeCustomerId, long quantity, PlanAddon addon, CancellationToken cancellationToken = default) + { + return null; + } + + public Task CreateStripeSessionForCustomerPortal(int departmentId, string stripeCustomerId, string customerConfigId, string emailAddress, string departmentName, CancellationToken cancellationToken = default) + { + return null; + } + + public Task GetActivePTTStripeSubscriptionAsync(string stripeCustomerId, CancellationToken cancellationToken = default) + { + return null; + } + + public Task CancelSubscriptionAsync(string stripeCustomerId, CancellationToken cancellationToken = default) + { + return null; + } } } diff --git a/Core/Resgrid.Services/QueueService.cs b/Core/Resgrid.Services/QueueService.cs index ec6850c1..594a12a0 100644 --- a/Core/Resgrid.Services/QueueService.cs +++ b/Core/Resgrid.Services/QueueService.cs @@ -34,6 +34,49 @@ public async Task GetQueueItemByIdAsync(int queueItemId) return await _queueItemsRepository.GetByIdAsync(queueItemId); } + public async Task GetPendingDeleteDepartmentQueueItemAsync(int departmentId) + { + var allItems = await _queueItemsRepository.GetAllAsync(); + var depItem = allItems.FirstOrDefault(x => + x.SourceId == departmentId.ToString() && x.ToBeCompletedOn > DateTime.UtcNow && x.QueueType == (int)QueueTypes.DeleteDepartment && x.CompletedOn == null); + + return depItem; + } + + public async Task> GetAllPendingDeleteDepartmentQueueItemsAsync() + { + var allItems = await _queueItemsRepository.GetAllAsync(); + var depItems = allItems.Where(x => + x.ToBeCompletedOn > DateTime.UtcNow && x.QueueType == (int)QueueTypes.DeleteDepartment && x.CompletedOn == null).ToList(); + + return depItems; + } + + public async Task EnqueuePendingDeleteDepartmentAsync(int departmentId, string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + QueueItem item = new QueueItem(); + item.QueueType = (int)QueueTypes.DeleteDepartment; + item.SourceId = departmentId.ToString(); + item.QueuedOn = DateTime.UtcNow; + item.ToBeCompletedOn = item.QueuedOn.AddDays(25); + item.QueuedByUserId = userId; + + var result = await _queueItemsRepository.SaveOrUpdateAsync(item, cancellationToken); + + return result; + } + + public async Task CancelPendingDepartmentDeletionRequest(int departmentId, string name, CancellationToken cancellationToken = default(CancellationToken)) + { + QueueItem item = await GetPendingDeleteDepartmentQueueItemAsync(departmentId); + item.CompletedOn = DateTime.UtcNow; + item.Data = $"{name} cancelled the department deletion request."; + + var result = await _queueItemsRepository.SaveOrUpdateAsync(item, cancellationToken); + + return result; + } + public async Task> DequeueAsync(QueueTypes type, CancellationToken cancellationToken = default(CancellationToken)) { var items = await _queueItemsRepository.GetPendingQueueItemsByTypeIdAsync((int)type); @@ -127,7 +170,10 @@ public async Task GetQueueItemByIdAsync(int queueItemId) cqi.Address = await _geoLocationProvider.GetAproxAddressFromLatLong(double.Parse(points[0]), double.Parse(points[1])); } } - catch { /* Ignore */ } + catch + { + /* Ignore */ + } } else { @@ -146,7 +192,7 @@ public async Task GetQueueItemByIdAsync(int queueItemId) //} if (cqi.Call.Attachments != null && - cqi.Call.Attachments.Count(x => x.CallAttachmentType == (int)CallAttachmentTypes.DispatchAudio) > 0) + cqi.Call.Attachments.Count(x => x.CallAttachmentType == (int)CallAttachmentTypes.DispatchAudio) > 0) { var audio = cqi.Call.Attachments.FirstOrDefault(x => x.CallAttachmentType == (int)CallAttachmentTypes.DispatchAudio); @@ -155,7 +201,7 @@ public async Task GetQueueItemByIdAsync(int queueItemId) cqi.CallDispatchAttachmentId = audio.CallAttachmentId; } - // We can't queue up any attachment data as it'll be too large. + // We can't queue up any attachment data as it'll be too large. cqi.Call.Attachments = null; return await _outboundQueueProvider.EnqueueCall(cqi); @@ -200,5 +246,10 @@ public async Task GetQueueItemByIdAsync(int queueItemId) return await _queueItemsRepository.SaveOrUpdateAsync(item, cancellationToken); } + + public async Task UpdateQueueItem(QueueItem item, CancellationToken cancellationToken = default(CancellationToken)) + { + return await _queueItemsRepository.SaveOrUpdateAsync(item, cancellationToken); + } } } diff --git a/Core/Resgrid.Services/Resgrid.Services.csproj b/Core/Resgrid.Services/Resgrid.Services.csproj index cd0bfd5b..4fdab1c1 100644 --- a/Core/Resgrid.Services/Resgrid.Services.csproj +++ b/Core/Resgrid.Services/Resgrid.Services.csproj @@ -28,6 +28,7 @@ + diff --git a/Core/Resgrid.Services/ScheduledTasksService.cs b/Core/Resgrid.Services/ScheduledTasksService.cs index c03621d6..5a4309fd 100644 --- a/Core/Resgrid.Services/ScheduledTasksService.cs +++ b/Core/Resgrid.Services/ScheduledTasksService.cs @@ -254,10 +254,10 @@ public async Task> GetUpcomingScheduledTasksAsync(DateTime c ///* We need to pull the Log for this schedule. There should only ever be 1 log per ScheduledTask //* per day, you can't run a scheduled task multiple times per day (at the current time) - //* + //* //* SJ 6/8/2014: I think a problem here was that we are tracking the log in UTC, but all other work is occuring - //* in a localized DateTime. We were seeing some very strange behavior when UTC ticked past midnight. I've modified - //* the selection below to run on a localized DateTime. Linq to Entities doesn't support time converion, so I broke + //* in a localized DateTime. We were seeing some very strange behavior when UTC ticked past midnight. I've modified + //* the selection below to run on a localized DateTime. Linq to Entities doesn't support time converion, so I broke //* up the query. //*/ //var logs = _scheduledTaskLogRepository.GetAllLogsForTask(scheduledTask.ScheduledTaskId); diff --git a/Core/Resgrid.Services/ShiftsService.cs b/Core/Resgrid.Services/ShiftsService.cs index 57b3dcc6..35563854 100644 --- a/Core/Resgrid.Services/ShiftsService.cs +++ b/Core/Resgrid.Services/ShiftsService.cs @@ -66,13 +66,19 @@ public async Task> GetAllShiftsByDepartmentAsync(int departmentId) public async Task GetShiftByIdAsync(int shiftId) { var shift = await _shiftsRepository.GetShiftAndDaysByShiftIdAsync(shiftId); - shift.Personnel = (await _shiftPersonRepository.GetAllShiftPersonsByShiftIdAsync(shiftId)).ToList(); - //shift.Department = await _departmentsService.GetDepartmentByIdAsync(shift.DepartmentId); - shift.Groups = await GetShiftGroupsForShift(shiftId); - //shift.Signups = (await _shiftSignupRepository.GetAllShiftSignupsByShiftIdAsync(shiftId)).ToList(); - //shift.Admins = (await _shift - return shift; + if (shift != null) + { + shift.Personnel = (await _shiftPersonRepository.GetAllShiftPersonsByShiftIdAsync(shiftId)).ToList(); + //shift.Department = await _departmentsService.GetDepartmentByIdAsync(shift.DepartmentId); + shift.Groups = await GetShiftGroupsForShift(shiftId); + //shift.Signups = (await _shiftSignupRepository.GetAllShiftSignupsByShiftIdAsync(shiftId)).ToList(); + //shift.Admins = (await _shift + + return shift; + } + + return null; } public async Task PopulateShiftData(Shift shift, bool getDepartment, bool getPersonnel, bool getGroups, diff --git a/Core/Resgrid.Services/SmsService.cs b/Core/Resgrid.Services/SmsService.cs index 4557e6d9..5f620c31 100644 --- a/Core/Resgrid.Services/SmsService.cs +++ b/Core/Resgrid.Services/SmsService.cs @@ -16,25 +16,38 @@ public class SmsService : ISmsService private readonly ITextMessageProvider _textMessageProvider; private readonly IDepartmentSettingsService _departmentSettingsService; private readonly IEmailSender _emailSender; + private readonly ISubscriptionsService _subscriptionsService; public SmsService(IUserProfileService userProfileService, IGeoLocationProvider geoLocationProvider, ITextMessageProvider textMessageProvider, IDepartmentSettingsService departmentSettingsService, - IEmailSender emailSender) + IEmailSender emailSender, ISubscriptionsService subscriptionsService) { _userProfileService = userProfileService; _geoLocationProvider = geoLocationProvider; _textMessageProvider = textMessageProvider; _departmentSettingsService = departmentSettingsService; _emailSender = emailSender; + _subscriptionsService = subscriptionsService; } - public async Task SendMessageAsync(Message message, string departmentNumber, int departmentId, UserProfile profile = null) + public async Task SendMessageAsync(Message message, string departmentNumber, int departmentId, UserProfile profile = null, Payment payment = null) { + if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId)) + return false; + if (profile == null && !String.IsNullOrWhiteSpace(message.ReceivingUserId)) profile = await _userProfileService.GetProfileByUserIdAsync(message.ReceivingUserId); MailMessage email = new MailMessage(); + /* Costs for sending SMS are getting a bit insane and due to the 'spammy' nature of these + * i.e. sending the same message to n recipients, it's a pain in the ass to explain that + * to the sms providers out there. Beacuse of that I have to turn off SMS functionality + * for the free departments and limit message functionality for some paid departments. -SJ 6-20-2023 + */ + if (payment != null && !_subscriptionsService.CanPlanSendMessageSms(payment.PlanId)) + return true; + if (profile != null && profile.SendMessageSms) { if (Config.SystemBehaviorConfig.DepartmentsToForceSmsGateway.Contains(departmentId)) @@ -72,7 +85,7 @@ await _textMessageProvider.SendTextMessage(profile.GetPhoneNumber(), FormatTextF return true; } - public async Task SendCallAsync(Call call, CallDispatch dispatch, string departmentNumber, int departmentId, UserProfile profile = null, string address = null) + public async Task SendCallAsync(Call call, CallDispatch dispatch, string departmentNumber, int departmentId, UserProfile profile = null, string address = null, Payment payment = null) { if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId)) return false; @@ -80,6 +93,14 @@ public async Task SendCallAsync(Call call, CallDispatch dispatch, string d if (profile == null) profile = await _userProfileService.GetProfileByUserIdAsync(dispatch.UserId); + /* Costs for sending SMS are getting a bit insane and due to the 'spammy' nature of these + * i.e. sending the same message to n recipients, it's a pain in the ass to explain that + * to the sms providers out there. Beacuse of that I have to turn off SMS functionality + * for the free departments and limit message functionality for some paid departments. -SJ 6-20-2023 + */ + if (payment != null && !_subscriptionsService.CanPlanSendCallSms(payment.PlanId)) + return true; + if (profile != null && profile.SendSms) { if (String.IsNullOrWhiteSpace(address)) @@ -178,8 +199,8 @@ public async Task SendCallAsync(Call call, CallDispatch dispatch, string d await _textMessageProvider.SendTextMessage(profile.GetPhoneNumber(), FormatTextForMessage(call.Name, text), departmentNumber, (MobileCarriers)profile.MobileCarrier, departmentId, false, true); - if (Config.SystemBehaviorConfig.SendCallsToSmsEmailGatewayAdditionally) - SendCallViaEmailSmsGateway(call, address, profile); + //if (Config.SystemBehaviorConfig.SendCallsToSmsEmailGatewayAdditionally) + // SendCallViaEmailSmsGateway(call, address, profile); } else { diff --git a/Core/Resgrid.Services/SubscriptionsService.cs b/Core/Resgrid.Services/SubscriptionsService.cs index 4ff7e60d..7463dfe8 100644 --- a/Core/Resgrid.Services/SubscriptionsService.cs +++ b/Core/Resgrid.Services/SubscriptionsService.cs @@ -502,5 +502,15 @@ public async Task SavePaymentAddonAsync(PaymentAddon paymentAddon, { return null; } + + public bool CanPlanSendMessageSms(int planId) + { + return true; + } + + public bool CanPlanSendCallSms(int planId) + { + return true; + } } } diff --git a/Core/Resgrid.Services/UnitsService.cs b/Core/Resgrid.Services/UnitsService.cs index 1c6ebc8b..9bcd03b0 100644 --- a/Core/Resgrid.Services/UnitsService.cs +++ b/Core/Resgrid.Services/UnitsService.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Resgrid.Framework; using Resgrid.Model; using Resgrid.Model.Events; using Resgrid.Model.Providers; @@ -26,12 +27,13 @@ public class UnitsService : IUnitsService private readonly IEventAggregator _eventAggregator; private readonly ICustomStateService _customStateService; private readonly IUnitActiveRolesRepository _unitActiveRolesRepository; + private readonly IDepartmentGroupsService _departmentGroupsService; public UnitsService(IUnitsRepository unitsRepository, IUnitStatesRepository unitStatesRepository, IUnitLogsRepository unitLogsRepository, IUnitTypesRepository unitTypesRepository, ISubscriptionsService subscriptionsService, IUnitRolesRepository unitRolesRepository, IUnitStateRoleRepository unitStateRoleRepository, IUserStateService userStateService, IEventAggregator eventAggregator, ICustomStateService customStateService, IMongoRepository unitLocationRepository, - IUnitActiveRolesRepository unitActiveRolesRepository) + IUnitActiveRolesRepository unitActiveRolesRepository, IDepartmentGroupsService departmentGroupsService) { _unitsRepository = unitsRepository; _unitStatesRepository = unitStatesRepository; @@ -45,6 +47,7 @@ public UnitsService(IUnitsRepository unitsRepository, IUnitStatesRepository unit _customStateService = customStateService; _unitLocationRepository = unitLocationRepository; _unitActiveRolesRepository = unitActiveRolesRepository; + _departmentGroupsService = departmentGroupsService; } public async Task> GetAllAsync() @@ -75,13 +78,18 @@ public async Task> GetUnitsForDepartmentAsync(int departmentId) List systemUnts = new List(); var units = (await _unitsRepository.GetAllUnitsByDepartmentIdAsync(departmentId)).ToList(); + var groups = await _departmentGroupsService.GetAllGroupsForDepartmentUnlimitedThinAsync(departmentId); int limit = (await _subscriptionsService.GetCurrentPlanForDepartmentAsync(departmentId)).GetLimitForTypeAsInt(PlanLimitTypes.Units); int count = units.Count < limit ? units.Count : limit; // Only return units up to the plans unit limit for (int i = 0; i < count; i++) { - units[i].Roles = await GetRolesForUnitAsync(units[i].UnitId); + //units[i].Roles = await GetRolesForUnitAsync(units[i].UnitId); + + if (units[i].StationGroupId.HasValue) + units[i].StationGroup = groups.FirstOrDefault(x => x.DepartmentGroupId == units[i].StationGroupId.Value); + systemUnts.Add(units[i]); } @@ -495,14 +503,14 @@ where callEnabledStates.Contains(us.State) public async Task AddUnitLocationAsync(UnitsLocation location, int departmentId, CancellationToken cancellationToken = default(CancellationToken)) { - if (location.Id.Timestamp == 0) - await _unitLocationRepository.InsertOneAsync(location); - else - await _unitLocationRepository.ReplaceOneAsync(location); - try { - _eventAggregator.SendMessage(new UnitLocationUpdatedEvent() + if (location.Id.Timestamp == 0) + await _unitLocationRepository.InsertOneAsync(location); + else + await _unitLocationRepository.ReplaceOneAsync(location); + + _eventAggregator.SendMessage(new UnitLocationUpdatedEvent() { DepartmentId = departmentId, UnitId = location.UnitId.ToString(), @@ -521,22 +529,37 @@ where callEnabledStates.Contains(us.State) public async Task GetLatestUnitLocationAsync(int unitId, DateTime? timestamp = null) { - var location = _unitLocationRepository.AsQueryable().Where(x => x.UnitId == unitId).OrderByDescending(y => y.Timestamp).FirstOrDefault(); + try + { + var location = _unitLocationRepository.AsQueryable().Where(x => x.UnitId == unitId).OrderByDescending(y => y.Timestamp).FirstOrDefault(); - //var layers = await _personnelLocationRepository.FilterByAsync(filter => filter.DepartmentId == departmentId && filter.Type == (int)type && filter.IsDeleted == false); + //var layers = await _personnelLocationRepository.FilterByAsync(filter => filter.DepartmentId == departmentId && filter.Type == (int)type && filter.IsDeleted == false); - return location; + return location; + } + catch (Exception ex) + { + Logging.LogException(ex); + } + + return null; } public async Task> GetLatestUnitLocationsAsync(int departmentId) { - var locations = _unitLocationRepository.AsQueryable().Where(x => x.DepartmentId == departmentId).OrderByDescending(y => y.Timestamp) - .GroupBy(x => x.UnitId).FirstOrDefault(); + try + { + var locations = _unitLocationRepository.AsQueryable().Where(x => x.DepartmentId == departmentId).OrderByDescending(y => y.Timestamp).GroupBy(x => x.UnitId).FirstOrDefault(); - //var layers = await _personnelLocationRepository.FilterByAsync(filter => filter.DepartmentId == departmentId && filter.Type == (int)type && filter.IsDeleted == false); + //var layers = await _personnelLocationRepository.FilterByAsync(filter => filter.DepartmentId == departmentId && filter.Type == (int)type && filter.IsDeleted == false); - if (locations != null) - return locations.ToList(); + if (locations != null) + return locations.ToList(); + } + catch (Exception ex) + { + Logging.LogException(ex); + } return new List(); } diff --git a/Core/Resgrid.Services/UsersService.cs b/Core/Resgrid.Services/UsersService.cs index ec6e5c8d..6606ff25 100644 --- a/Core/Resgrid.Services/UsersService.cs +++ b/Core/Resgrid.Services/UsersService.cs @@ -10,6 +10,7 @@ using MongoDB.Bson; using System.Collections.ObjectModel; using Resgrid.Model.Events; +using Resgrid.Framework; namespace Resgrid.Services { @@ -222,13 +223,13 @@ public int GetUsersCount() public async Task SavePersonnelLocationAsync(PersonnelLocation personnelLocation) { - if (personnelLocation.Id.Timestamp == 0) - await _personnelLocationRepository.InsertOneAsync(personnelLocation); - else - await _personnelLocationRepository.ReplaceOneAsync(personnelLocation); - try { + if (personnelLocation.Id.Timestamp == 0) + await _personnelLocationRepository.InsertOneAsync(personnelLocation); + else + await _personnelLocationRepository.ReplaceOneAsync(personnelLocation); + _eventAggregator.SendMessage(new PersonnelLocationUpdatedEvent() { DepartmentId = personnelLocation.DepartmentId, UserId = personnelLocation.UserId, @@ -286,22 +287,46 @@ public async Task> GetLatestLocationsForDepartmentPerson var result = await _personnelLocationRepository.Aggregate(pipeline); */ - var locations = _personnelLocationRepository.AsQueryable().Where(x => x.DepartmentId == departmentId).OrderByDescending(y => y.Timestamp) - .GroupBy(x => x.UserId).FirstOrDefault(); + try + { + var locations = _personnelLocationRepository.AsQueryable().Where(x => x.DepartmentId == departmentId).OrderByDescending(y => y.Timestamp) + .GroupBy(x => x.UserId).FirstOrDefault(); - //var layers = await _personnelLocationRepository.FilterByAsync(filter => filter.DepartmentId == departmentId && filter.Type == (int)type && filter.IsDeleted == false); + //var layers = await _personnelLocationRepository.FilterByAsync(filter => filter.DepartmentId == departmentId && filter.Type == (int)type && filter.IsDeleted == false); - if (locations != null) - return locations.ToList(); + if (locations != null) + return locations.ToList(); + } + catch (Exception ex) + { + Logging.LogException(ex); + } return new List(); } public async Task GetPersonnelLocationByIdAsync(string id) { - var layers = await _personnelLocationRepository.FindByIdAsync(id); + try + { + var layers = await _personnelLocationRepository.FindByIdAsync(id); + + return layers; + } + catch (Exception ex) + { + Logging.LogException(ex); + } + + return null; + } + + public async Task ClearOutUserLoginAsync(string userId) + { + await _identityRepository.ClearOutUserLoginAsync(userId); + await _identityRepository.CleanUpOIDCTokensByUserAsync(userId); - return layers; + return true; } } } diff --git a/Core/Resgrid.Services/VoiceService.cs b/Core/Resgrid.Services/VoiceService.cs index d8776d61..fff44ffe 100644 --- a/Core/Resgrid.Services/VoiceService.cs +++ b/Core/Resgrid.Services/VoiceService.cs @@ -1,10 +1,12 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Resgrid.Model; using Resgrid.Model.Providers; using Resgrid.Model.Repositories; using Resgrid.Model.Services; +using Resgrid.Providers.Voip; namespace Resgrid.Services { @@ -17,10 +19,11 @@ public class VoiceService : IVoiceService private readonly IDepartmentsService _departmentsService; private readonly IVoipProvider _voipProvider; private readonly IUserProfileService _userProfileService; + private readonly IDepartmentAudioRepository _departmentAudioRepository; public VoiceService(IDepartmentVoiceRepository departmentVoiceRepository, IDepartmentVoiceChannelRepository departmentVoiceChannelRepository, IDepartmentVoiceUserRepository departmentVoiceUserRepository, ISubscriptionsService subscriptionsService, IDepartmentsService departmentsService, - IVoipProvider voipProvider, IUserProfileService userProfileService) + IVoipProvider voipProvider, IUserProfileService userProfileService, IDepartmentAudioRepository departmentAudioRepository) { _departmentVoiceRepository = departmentVoiceRepository; _departmentVoiceChannelRepository = departmentVoiceChannelRepository; @@ -29,6 +32,7 @@ public VoiceService(IDepartmentVoiceRepository departmentVoiceRepository, IDepar _departmentsService = departmentsService; _voipProvider = voipProvider; _userProfileService = userProfileService; + _departmentAudioRepository = departmentAudioRepository; } public async Task CanDepartmentUseVoiceAsync(int departmentId) @@ -252,5 +256,63 @@ public async Task GetVoiceChannelByIdAsync(string voiceC return await _departmentVoiceChannelRepository.SaveOrUpdateAsync(voiceChannel, cancellationToken, true); } + + public async Task GetCurrentUtilizationForLiveKit(int departmentId) + { + var result = new DepartmentVoiceUtilization(); + var addonPlans = await _subscriptionsService.GetAllAddonPlansByTypeAsync(PlanAddonTypes.PTT); + var addonPayment = await _subscriptionsService.GetCurrentPaymentAddonsForDepartmentAsync(departmentId, addonPlans.Select(x => x.PlanAddonId).ToList()); + + if (addonPayment != null && addonPayment.Count > 0) + { + foreach (var payment in addonPayment) + { + result.SeatLimit += (int)(payment.Quantity * 10); + } + + var livekitProvider = new LiveKitProvider(); + var voiceChannels = await _departmentVoiceChannelRepository.GetDepartmentVoiceChannelByDepartmentIdAsync(departmentId); + + if (voiceChannels != null) + { + foreach (var channel in voiceChannels) + { + var participants = await livekitProvider.ListRoomParticipants(channel.DepartmentVoiceChannelId); + + if (participants != null) + result.CurrentlyActive += participants.Count(); + } + } + } + + return result; + } + + public async Task SaveDepartmentAudioAsync(DepartmentAudio departmentAudio, CancellationToken cancellationToken = default(CancellationToken)) + { + return await _departmentAudioRepository.SaveOrUpdateAsync(departmentAudio, cancellationToken); + } + + public async Task> GetDepartmentAudiosByDepartmentIdAsync(int departmentId) + { + var items = await _departmentAudioRepository.GetAllByDepartmentIdAsync(departmentId); + + if (items != null) + return items.ToList(); + + return new List(); + } + + public async Task GetDepartmentAudioByIdAsync(string id) + { + return await _departmentAudioRepository.GetByIdAsync(id); + } + + public async Task DeleteDepartmentAudioAsync(DepartmentAudio audio, CancellationToken cancellationToken = default(CancellationToken)) + { + var result = await _departmentAudioRepository.DeleteAsync(audio, cancellationToken); + + return result; + } } } diff --git a/Providers/Resgrid.Providers.AddressVerification/LoqateProvider.cs b/Providers/Resgrid.Providers.AddressVerification/LoqateProvider.cs index 304247e8..0b24ad75 100644 --- a/Providers/Resgrid.Providers.AddressVerification/LoqateProvider.cs +++ b/Providers/Resgrid.Providers.AddressVerification/LoqateProvider.cs @@ -26,6 +26,12 @@ public async Task VerifyAddressAsync(Address address) if (response.StatusCode != HttpStatusCode.OK) result.ServiceSuccess = false; + if (response.Data == null) + { + result.ServiceSuccess = false; + return result; + } + if (response.Data.status != "OK") result.ServiceSuccess = false; diff --git a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundEventProvider.cs b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundEventProvider.cs index 170ba902..e08813fd 100644 --- a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundEventProvider.cs +++ b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundEventProvider.cs @@ -15,7 +15,7 @@ namespace Resgrid.Providers.Bus.Rabbit { public class RabbitInboundEventProvider : IRabbitInboundEventProvider { - private ConnectionFactory _factory; + //private ConnectionFactory _factory; private IConnection _connection; private IModel _channel; @@ -36,46 +36,24 @@ public async Task Start() private void VerifyAndCreateClients() { - // I know....I know..... try { - _factory = new ConnectionFactory() { HostName = ServiceBusConfig.RabbitHostname, UserName = ServiceBusConfig.RabbitUsername, Password = ServiceBusConfig.RabbbitPassword }; - _connection = _factory.CreateConnection(); - } - catch (Exception ex) - { - Logging.LogException(ex); + _connection = RabbitConnection.CreateConnection(); - if (!String.IsNullOrWhiteSpace(ServiceBusConfig.RabbitHostname2)) + if (_connection != null) { - try - { - _factory = new ConnectionFactory() { HostName = ServiceBusConfig.RabbitHostname2, UserName = ServiceBusConfig.RabbitUsername, Password = ServiceBusConfig.RabbbitPassword }; - _connection = _factory.CreateConnection(); - } - catch (Exception ex2) - { - Logging.LogException(ex2); - + _channel = _connection.CreateModel(); - if (!String.IsNullOrWhiteSpace(ServiceBusConfig.RabbitHostname3)) - { - try - { - _factory = new ConnectionFactory() { HostName = ServiceBusConfig.RabbitHostname3, UserName = ServiceBusConfig.RabbitUsername, Password = ServiceBusConfig.RabbbitPassword }; - _connection = _factory.CreateConnection(); - } - catch (Exception ex3) - { - Logging.LogException(ex3); - throw; - } - } + if (_channel != null) + { + _channel.ExchangeDeclare(SetQueueNameForEnv(Topics.EventingTopic), "fanout"); } } } - - _channel = _connection.CreateModel(); + catch (Exception ex) + { + Framework.Logging.LogException(ex); + } } private async Task StartMonitoring() diff --git a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundQueueProvider.cs b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundQueueProvider.cs index 3c874bb2..7c9beb76 100644 --- a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundQueueProvider.cs +++ b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitInboundQueueProvider.cs @@ -526,13 +526,15 @@ private bool RetryQueueItem(BasicDeliverEventArgs ea, Exception mex) { IBasicProperties props = channel.CreateBasicProperties(); props.DeliveryMode = 2; - props.Expiration = "36000000"; - props.Headers = new Dictionary(); - props.Headers.Add("x-redelivered-count", currentDeliveryCount++); - props.Headers.Add("x-previous-error", mex.Message); + + // I *THINK* these headers are appearing in the body when trying to deserialze and it's blowing up protobuf. -SJ + //props.Expiration = "36000000"; + //props.Headers = new Dictionary(); + //props.Headers.Add("x-redelivered-count", currentDeliveryCount++); + //props.Headers.Add("x-previous-error", mex.Message); // https://github.com/rabbitmq/rabbitmq-delayed-message-exchange - props.Headers.Add("x-delay", 5000); + //props.Headers.Add("x-delay", 5000); channel.BasicPublish(exchange: ea.Exchange, routingKey: ea.RoutingKey, diff --git a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs index 11cebc19..40ef89ce 100644 --- a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs +++ b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs @@ -117,26 +117,23 @@ public bool UnitLocationUpdatedChanged(UnitLocationUpdatedEvent message) private static void VerifyAndCreateClients() { - if (SystemBehaviorConfig.ServiceBusType == ServiceBusTypes.Rabbit) + try { - try - { - //var factory = new ConnectionFactory() { HostName = ServiceBusConfig.RabbitHostname, UserName = ServiceBusConfig.RabbitUsername, Password = ServiceBusConfig.RabbbitPassword }; - //using (var connection = factory.CreateConnection()) - var connection = RabbitConnection.CreateConnection(); + //var factory = new ConnectionFactory() { HostName = ServiceBusConfig.RabbitHostname, UserName = ServiceBusConfig.RabbitUsername, Password = ServiceBusConfig.RabbbitPassword }; + //using (var connection = factory.CreateConnection()) + var connection = RabbitConnection.CreateConnection(); - if (connection != null) + if (connection != null) + { + using (var channel = connection.CreateModel()) { - using (var channel = connection.CreateModel()) - { - channel.ExchangeDeclare(SetQueueNameForEnv(Topics.EventingTopic), "fanout"); - } + channel.ExchangeDeclare(SetQueueNameForEnv(Topics.EventingTopic), "fanout"); } } - catch (Exception ex) - { - Framework.Logging.LogException(ex); - } + } + catch (Exception ex) + { + Framework.Logging.LogException(ex); } } diff --git a/Providers/Resgrid.Providers.Bus/SignalrProvider.cs b/Providers/Resgrid.Providers.Bus/SignalrProvider.cs index df4675da..a49fac86 100644 --- a/Providers/Resgrid.Providers.Bus/SignalrProvider.cs +++ b/Providers/Resgrid.Providers.Bus/SignalrProvider.cs @@ -29,7 +29,7 @@ public async Task PersonnelStatusUpdated(int departmentId, ActionLog actio catch (Exception e) { // Disabling due to unnecessary logging of redundant exceptions. - Logging.LogException(e); + //Logging.LogException(e); } return false; @@ -46,7 +46,7 @@ public async Task PersonnelStaffingUpdated(int departmentId, UserState use catch (Exception e) { // Disabling due to unnecessary logging of redundant exceptions. - Logging.LogException(e); + //Logging.LogException(e); } return false; @@ -63,7 +63,7 @@ public async Task UnitStatusUpdated(int departmentId, UnitState unitState) catch (Exception e) { // Disabling due to unnecessary logging of redundant exceptions. - Logging.LogException(e); + //Logging.LogException(e); } return false; @@ -80,7 +80,7 @@ public async Task CallsUpdated(int departmentId, Call call) catch (Exception e) { // Disabling due to unnecessary logging of redundant exceptions. - Logging.LogException(e); + //Logging.LogException(e); } return false; @@ -122,7 +122,7 @@ private async Task Connect() } catch (Exception ex) { - Logging.LogException(ex); + //Logging.LogException(ex); //Create(); } } diff --git a/Providers/Resgrid.Providers.Cache/Resgrid.Providers.Cache.csproj b/Providers/Resgrid.Providers.Cache/Resgrid.Providers.Cache.csproj index c464471b..ed5c86ba 100644 --- a/Providers/Resgrid.Providers.Cache/Resgrid.Providers.Cache.csproj +++ b/Providers/Resgrid.Providers.Cache/Resgrid.Providers.Cache.csproj @@ -7,7 +7,7 @@ - + diff --git a/Providers/Resgrid.Providers.Email/EmailSender.cs b/Providers/Resgrid.Providers.Email/EmailSender.cs index ce260480..8ec201bc 100644 --- a/Providers/Resgrid.Providers.Email/EmailSender.cs +++ b/Providers/Resgrid.Providers.Email/EmailSender.cs @@ -38,7 +38,9 @@ public async Task Send(Email email) return true; } - catch (InvalidOperationException) { } + catch (InvalidOperationException) + { + } } return false; @@ -55,7 +57,9 @@ public async Task SendEmail(MailMessage email) return true; } - catch (InvalidOperationException) { } + catch (InvalidOperationException) + { + } return false; } @@ -75,38 +79,38 @@ private static Func CreateDefaultClientFactory() private static Func CreateDefaultMailMessageFactory() { return email => - { - var message = new MailMessage { From = new MailAddress(email.From), Subject = email.Subject }; - - if (!string.IsNullOrEmpty(email.Sender)) - { - message.Sender = new MailAddress(email.Sender); - } - - email.To.Each(to => message.To.Add(to)); - email.ReplyTo.Each(to => message.ReplyToList.Add(to)); - email.CC.Each(cc => message.CC.Add(cc)); - email.Bcc.Each(bcc => message.Bcc.Add(bcc)); - email.Headers.Each(pair => message.Headers[pair.Key] = pair.Value); - - if (!string.IsNullOrEmpty(email.HtmlBody) && !string.IsNullOrEmpty(email.TextBody)) - { - message.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(email.HtmlBody, new ContentType(ContentTypes.Html))); - //message.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(email.TextBody, new ContentType(ContentTypes.Text))); - } - else if (!string.IsNullOrEmpty(email.HtmlBody)) - { - message.Body = email.HtmlBody; - message.IsBodyHtml = true; - } - else if (!string.IsNullOrEmpty(email.TextBody)) - { - message.Body = email.TextBody; - message.IsBodyHtml = false; - } - - return message; - }; + { + var message = new MailMessage { From = new MailAddress(email.From), Subject = email.Subject }; + + if (!string.IsNullOrEmpty(email.Sender)) + { + message.Sender = new MailAddress(email.Sender); + } + + email.To.Each(to => message.To.Add(to)); + email.ReplyTo.Each(to => message.ReplyToList.Add(to)); + email.CC.Each(cc => message.CC.Add(cc)); + email.Bcc.Each(bcc => message.Bcc.Add(bcc)); + email.Headers.Each(pair => message.Headers[pair.Key] = pair.Value); + + if (!string.IsNullOrEmpty(email.HtmlBody) && !string.IsNullOrEmpty(email.TextBody)) + { + message.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(email.HtmlBody, new ContentType(ContentTypes.Html))); + //message.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(email.TextBody, new ContentType(ContentTypes.Text))); + } + else if (!string.IsNullOrEmpty(email.HtmlBody)) + { + message.Body = email.HtmlBody; + message.IsBodyHtml = true; + } + else if (!string.IsNullOrEmpty(email.TextBody)) + { + message.Body = email.TextBody; + message.IsBodyHtml = false; + } + + return message; + }; } } } diff --git a/Providers/Resgrid.Providers.Email/PostmarkTemplateProvider.cs b/Providers/Resgrid.Providers.Email/PostmarkTemplateProvider.cs index a261d0d1..29f49e9c 100644 --- a/Providers/Resgrid.Providers.Email/PostmarkTemplateProvider.cs +++ b/Providers/Resgrid.Providers.Email/PostmarkTemplateProvider.cs @@ -30,7 +30,6 @@ public PostmarkTemplateProvider(IEmailSender emailSender) public void Configure(object sender, string fromAddress) { - } public void SendAffiliateRegister(string email, string affiliateCode) @@ -48,29 +47,68 @@ public void SendAffiliateWelcomeMail(string name, string email) throw new NotImplementedException(); } + + public async Task SendDeleteDepartmentEmail(string requesterName, string departmentName, DateTime localCompletedOn, string sendingToPersonName, string email) + { + + var templateModel = new Dictionary + { + { "requester_name", requesterName }, + { "department_name", departmentName }, + { "local_deletion_date", localCompletedOn.ToString("F") }, + { "name", sendingToPersonName }, + { "login_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/Account/LogOn" }, + { "support_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/Home/Contact" }, + }; + + try + { + var template = Mustachio.Parser.Parse(GetTempate("DeleteDepartment.html")); + var content = template(templateModel); + + Email newEmail = new Email(); + newEmail.HtmlBody = content; + newEmail.Sender = FROM_EMAIL; + newEmail.From = FROM_EMAIL; + newEmail.Subject = "Resgrid Department Deletion Request"; + newEmail.To.Add(email); + + return await _emailSender.Send(newEmail); + } + catch (Exception) + { + } + + + return false; + } + + public async Task SendCallMail(string email, string subject, string title, string priority, string natureOfCall, string mapPage, string address, string dispatchedOn, int callId, string userId, string coordinates, string shortenedAudioUrl) { - string callQuery = String.Empty; try { - callQuery = Convert.ToBase64String(Encoding.UTF8.GetBytes(SymmetricEncryption.Encrypt(callId.ToString(), Config.SystemBehaviorConfig.ExternalLinkUrlParamPassphrase))); + callQuery = Convert.ToBase64String( + Encoding.UTF8.GetBytes(SymmetricEncryption.Encrypt(callId.ToString(), Config.SystemBehaviorConfig.ExternalLinkUrlParamPassphrase))); + } + catch + { } - catch { } var templateModel = new Dictionary { - {"subject", title}, - {"date", dispatchedOn}, - {"nature", HtmlToTextHelper.ConvertHtml(natureOfCall)}, - {"priority", priority}, - {"address", address}, - {"map_page", mapPage}, - {"action_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/User/Dispatch/CallExportEx?query={callQuery}"}, - {"userId", userId}, - {"coordinates", coordinates} + { "subject", title }, + { "date", dispatchedOn }, + { "nature", HtmlToTextHelper.ConvertHtml(natureOfCall) }, + { "priority", priority }, + { "address", address }, + { "map_page", mapPage }, + { "action_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/User/Dispatch/CallExportEx?query={callQuery}" }, + { "userId", userId }, + { "coordinates", coordinates } }; if (!String.IsNullOrWhiteSpace(shortenedAudioUrl)) @@ -102,7 +140,9 @@ public async Task SendCallMail(string email, string subject, string title, return true; } - catch (Exception) { } + catch (Exception) + { + } } else { @@ -118,13 +158,16 @@ public async Task SendCallMail(string email, string subject, string title, return await _emailSender.Send(newEmail); } - catch (Exception) { } + catch (Exception) + { + } } return false; } - public async Task SendTroubleAlertMail(string email, string unitName, string gpsLocation, string personnel, string callAddress, string unitAddress, string dispatchedOn, string callName) + public async Task SendTroubleAlertMail(string email, string unitName, string gpsLocation, string personnel, string callAddress, string unitAddress, + string dispatchedOn, string callName) { // Example request var message = new TemplatedPostmarkMessage @@ -132,7 +175,8 @@ public async Task SendTroubleAlertMail(string email, string unitName, stri From = DONOTREPLY_EMAIL, To = email, TemplateId = Config.OutboundEmailServerConfig.PostmarkTroubleAlertTemplateId, - TemplateModel = new Dictionary { + TemplateModel = new Dictionary + { { "unit_name", unitName }, { "date", dispatchedOn }, { "active_call", callName }, @@ -158,11 +202,12 @@ public async Task SendTroubleAlertMail(string email, string unitName, stri return true; } - catch (Exception) { } + catch (Exception) + { + } } else { - } return false; @@ -175,16 +220,17 @@ public async Task SendCancellationReciept(string name, string email, strin From = FROM_EMAIL, To = email, TemplateId = Config.OutboundEmailServerConfig.PostmarkCancelRecieptTemplateId, - TemplateModel = new Dictionary { - { "action_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/User/Subscription" }, - { "subscriptions_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/User/Subscription" }, - { "feedback_url", LIVECHAT_URL }, - { "help_url", HELP_URL }, - { "trial_extension_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/User/Subscription" }, - { "export_url", "" }, - { "plans_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/Home/Pricing" }, - { "close_account_url", HELP_URL}, - }, + TemplateModel = new Dictionary + { + { "action_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/User/Subscription" }, + { "subscriptions_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/User/Subscription" }, + { "feedback_url", LIVECHAT_URL }, + { "help_url", HELP_URL }, + { "trial_extension_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/User/Subscription" }, + { "export_url", "" }, + { "plans_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/Home/Pricing" }, + { "close_account_url", HELP_URL }, + }, }; var client = new PostmarkClient(Config.OutboundEmailServerConfig.PostmarkApiKey); @@ -199,7 +245,9 @@ public async Task SendCancellationReciept(string name, string email, strin return true; } - catch (Exception) { } + catch (Exception) + { + } return false; } @@ -212,7 +260,8 @@ public async Task SendChargeFailed(string name, string email, string endDa From = FROM_EMAIL, To = email, TemplateId = Config.OutboundEmailServerConfig.PostmarkChargeFailedTemplateId, - TemplateModel = new Dictionary { + TemplateModel = new Dictionary + { { "plan_name", planName }, { "action_url", UPDATEBILLINGINFO_URL }, { "subscriptions_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/User/Subscription" }, @@ -236,7 +285,9 @@ public async Task SendChargeFailed(string name, string email, string endDa return true; } - catch (Exception) { } + catch (Exception) + { + } return false; } @@ -249,7 +300,8 @@ public async Task SendInviteMail(string code, string departmentName, strin From = FROM_EMAIL, To = email, TemplateId = Config.OutboundEmailServerConfig.PostmarkInviteTemplateId, - TemplateModel = new Dictionary { + TemplateModel = new Dictionary + { { "invite_sender_name", senderName }, { "department_name", departmentName }, { "action_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/Account/CompleteInvite?inviteCode={code}" }, @@ -273,31 +325,35 @@ public async Task SendInviteMail(string code, string departmentName, strin return true; } - catch (Exception) { } + catch (Exception) + { + } return false; } - public async Task SendMessageMail(string email, string subject, string messageSubject, string messageBody, string senderEmail, string senderName, string sentOn, int messageId) + public async Task SendMessageMail(string email, string subject, string messageSubject, string messageBody, string senderEmail, string senderName, string sentOn, + int messageId) { - var templateModel = new Dictionary { - { "sender_name", senderName }, - { "title", subject }, - { "body", HtmlToTextHelper.ConvertHtml(messageBody) }, - //{ "attachment_details", new []{ - //new Dictionary { - // { "attachmnet_url", "attachmnet_url_Value" }, - // { "attachment_name", "attachment_name_Value" }, - // { "attachment_size", "attachment_size_Value" }, - // { "attachment_type", "attachment_type_Value" }, - //} - //} - //}, - - { "action_url", $"https://resgrid.com/User/Messages/ViewMessage?messageId={messageId}" }, - { "timestamp", sentOn }, - { "commenter_name", senderName } - }; + var templateModel = new Dictionary + { + { "sender_name", senderName }, + { "title", subject }, + { "body", HtmlToTextHelper.ConvertHtml(messageBody) }, + //{ "attachment_details", new []{ + //new Dictionary { + // { "attachmnet_url", "attachmnet_url_Value" }, + // { "attachment_name", "attachment_name_Value" }, + // { "attachment_size", "attachment_size_Value" }, + // { "attachment_type", "attachment_type_Value" }, + //} + //} + //}, + + { "action_url", $"https://resgrid.com/User/Messages/ViewMessage?messageId={messageId}" }, + { "timestamp", sentOn }, + { "commenter_name", senderName } + }; if (SystemBehaviorConfig.OutboundEmailType == OutboundEmailTypes.Postmark) { @@ -321,7 +377,9 @@ public async Task SendMessageMail(string email, string subject, string mes return true; } - catch (Exception) { } + catch (Exception) + { + } } else { @@ -337,7 +395,9 @@ public async Task SendMessageMail(string email, string subject, string mes return await _emailSender.Send(newEmail); } - catch (Exception) { } + catch (Exception) + { + } } return false; @@ -345,17 +405,18 @@ public async Task SendMessageMail(string email, string subject, string mes public async Task SendPasswordResetMail(string name, string password, string userName, string email, string departmentName) { - var templateModel = new Dictionary { - { "name", name }, - { "department_Name", departmentName }, - { "login_url", LOGIN_URL }, - { "username", userName }, - { "password", password }, - { "support_url", LIVECHAT_URL }, - { "action_url", LOGIN_URL }, - { "operating_system", "" }, - { "browser_name", "" }, - }; + var templateModel = new Dictionary + { + { "name", name }, + { "department_Name", departmentName }, + { "login_url", LOGIN_URL }, + { "username", userName }, + { "password", password }, + { "support_url", LIVECHAT_URL }, + { "action_url", LOGIN_URL }, + { "operating_system", "" }, + { "browser_name", "" }, + }; if (SystemBehaviorConfig.OutboundEmailType == OutboundEmailTypes.Postmark) { @@ -379,7 +440,9 @@ public async Task SendPasswordResetMail(string name, string password, stri return true; } - catch (Exception) { } + catch (Exception) + { + } } else { @@ -395,7 +458,9 @@ public async Task SendPasswordResetMail(string name, string password, stri return await _emailSender.Send(newEmail); } - catch (Exception) { } + catch (Exception) + { + } } return false; @@ -409,15 +474,19 @@ public async Task SendPaymentReciept(string departmentName, string name, s From = FROM_EMAIL, To = email, TemplateId = Config.OutboundEmailServerConfig.PostmarkRecieptTemplateId, - TemplateModel = new Dictionary { + TemplateModel = new Dictionary + { { "purchase_date", processDate }, { "name", name }, { "billing_url", UPDATEBILLINGINFO_URL }, { "uservoice_url", LIVECHAT_URL }, { "receipt_id", transactionId }, { "date", effectiveDates }, - { "receipt_details", new []{ - new Dictionary { + { + "receipt_details", new[] + { + new Dictionary + { { "description", planName }, { "amount", amount } } @@ -444,12 +513,15 @@ public async Task SendPaymentReciept(string departmentName, string name, s return true; } - catch (Exception) { } + catch (Exception) + { + } return false; } - public async Task SendRefundReciept(string name, string email, string departmentName, string processDate, string amount, string processor, string transactionId, string originalPaymentId) + public async Task SendRefundReciept(string name, string email, string departmentName, string processDate, string amount, string processor, string transactionId, + string originalPaymentId) { throw new NotImplementedException(); } @@ -459,25 +531,27 @@ public async Task SendSignupMail(string name, string departmentName, strin throw new NotImplementedException(); } - public async Task SendUpgradePaymentReciept(string departmentName, string processDate, string amount, string email, string processor, string transactionId, string planName, string newPlanName, string effectiveDates, string nextBillingDate) + public async Task SendUpgradePaymentReciept(string departmentName, string processDate, string amount, string email, string processor, string transactionId, + string planName, string newPlanName, string effectiveDates, string nextBillingDate) { throw new NotImplementedException(); } public async Task SendWelcomeMail(string name, string departmentName, string userName, string password, string email, int departmentId) { - var templateModel = new Dictionary { - { "name", name }, - { "action_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}" }, - { "login_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/Account/LogOn" }, - { "department_id", departmentId }, - { "department_name", departmentName }, - { "username", userName }, - { "password", password }, - { "support_email", FROM_EMAIL }, - { "live_chat_url", LIVECHAT_URL }, - { "help_url", HELP_URL }, - }; + var templateModel = new Dictionary + { + { "name", name }, + { "action_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}" }, + { "login_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/Account/LogOn" }, + { "department_id", departmentId }, + { "department_name", departmentName }, + { "username", userName }, + { "password", password }, + { "support_email", FROM_EMAIL }, + { "live_chat_url", LIVECHAT_URL }, + { "help_url", HELP_URL }, + }; if (SystemBehaviorConfig.OutboundEmailType == OutboundEmailTypes.Postmark) { @@ -501,7 +575,9 @@ public async Task SendWelcomeMail(string name, string departmentName, stri return true; } - catch (Exception) { } + catch (Exception) + { + } } else { @@ -517,18 +593,22 @@ public async Task SendWelcomeMail(string name, string departmentName, stri return await _emailSender.Send(newEmail); } - catch (Exception) { } + catch (Exception) + { + } } return false; } - public async Task TEAM_SendNofifySubCancelled(string name, string email, string departmentName, string departmentId, string reason, string processedOn, string planName, string refundIssued) + public async Task TEAM_SendNofifySubCancelled(string name, string email, string departmentName, string departmentId, string reason, string processedOn, + string planName, string refundIssued) { throw new NotImplementedException(); } - public async Task TEAM_SendNotifyRefundIssued(string departmentId, string departmentName, string processDate, string amount, string processor, string transactionId, string originalPaymentId) + public async Task TEAM_SendNotifyRefundIssued(string departmentId, string departmentName, string processDate, string amount, string processor, string transactionId, + string originalPaymentId) { throw new NotImplementedException(); } @@ -541,7 +621,8 @@ public async Task SendNewDepartmentLinkMail(string name, string department From = FROM_EMAIL, To = email, TemplateId = Config.OutboundEmailServerConfig.PostmarkNewDepLinkTemplateId, - TemplateModel = new Dictionary { + TemplateModel = new Dictionary + { { "name", name }, { "action_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}" }, { "login_url", $"{Config.SystemBehaviorConfig.ResgridBaseUrl}/Account/LogOn" }, @@ -565,14 +646,17 @@ public async Task SendNewDepartmentLinkMail(string name, string department return true; } - catch (Exception) { } + catch (Exception) + { + } return false; } private string GetTempate(string templateName) { - using (var resource = typeof(PostmarkTemplateProvider).Assembly.GetManifestResourceStream(templateName)) + var assembly = typeof(PostmarkTemplateProvider).Assembly; + using (var resource = assembly.GetManifestResourceStream(assembly.GetName().Name + ".Template." + templateName)) { using (var reader = new StreamReader(resource)) { diff --git a/Providers/Resgrid.Providers.Email/Resgrid.Providers.Email.csproj b/Providers/Resgrid.Providers.Email/Resgrid.Providers.Email.csproj index e60a49a0..552c9956 100644 --- a/Providers/Resgrid.Providers.Email/Resgrid.Providers.Email.csproj +++ b/Providers/Resgrid.Providers.Email/Resgrid.Providers.Email.csproj @@ -16,6 +16,7 @@ + diff --git a/Providers/Resgrid.Providers.Email/Template/DeleteDepartment.html b/Providers/Resgrid.Providers.Email/Template/DeleteDepartment.html new file mode 100644 index 00000000..27a4aaeb --- /dev/null +++ b/Providers/Resgrid.Providers.Email/Template/DeleteDepartment.html @@ -0,0 +1,475 @@ + + + + + + Resgrid Department Deletion Request + + + + + Your Resgrid Department is Scheduled for Deletion + + + + + + + diff --git a/Providers/Resgrid.Providers.Marketing/MailerliteEmailMarketing.cs b/Providers/Resgrid.Providers.Marketing/MailerliteEmailMarketing.cs index e466af2d..c1b72e16 100644 --- a/Providers/Resgrid.Providers.Marketing/MailerliteEmailMarketing.cs +++ b/Providers/Resgrid.Providers.Marketing/MailerliteEmailMarketing.cs @@ -9,49 +9,49 @@ public class MailerliteEmailMarketing : IEmailMarketingProvider { public async Task Unsubscribe(string emailAddress) { - try - { - var client = new RestClient(Config.MarketingConfig.MailerlteUrl); - var request = new RestRequest("/api/v1/subscribers/unsubscribe/", Method.Post); - request.AddObject(new - { - apiKey = Config.MarketingConfig.MailingApiKey, - email = emailAddress - }); - var response = await client.ExecuteAsync(request); + //try + //{ + // var client = new RestClient(Config.MarketingConfig.MailerlteUrl); + // var request = new RestRequest("/api/v1/subscribers/unsubscribe/", Method.Post); + // request.AddObject(new + // { + // apiKey = Config.MarketingConfig.MailingApiKey, + // email = emailAddress + // }); + // var response = await client.ExecuteAsync(request); - return true; - } - catch { } + // return true; + //} + //catch { } return false; } public async Task SubscribeUserToAdminList(string firstName, string lastName, string emailAddress) { - try - { - var client = new RestClient(Config.MarketingConfig.MailerlteUrl); - var request = new RestRequest(string.Format("api/v1/subscribers/{0}/", Config.MarketingConfig.AdminListId), Method.Post); - request.AddObject(new - { - apiKey = Config.MarketingConfig.MailingApiKey, - email = emailAddress, - name = firstName, - fields = new List - { - new - { - name = "last_name", - value = lastName - } - } - }); - var response = await client.ExecuteAsync(request); + //try + //{ + // var client = new RestClient(Config.MarketingConfig.MailerlteUrl); + // var request = new RestRequest(string.Format("api/v1/subscribers/{0}/", Config.MarketingConfig.AdminListId), Method.Post); + // request.AddObject(new + // { + // apiKey = Config.MarketingConfig.MailingApiKey, + // email = emailAddress, + // name = firstName, + // fields = new List + // { + // new + // { + // name = "last_name", + // value = lastName + // } + // } + // }); + // var response = await client.ExecuteAsync(request); - return true; - } - catch { } + // return true; + //} + //catch { } return false; } diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0020_AddingLangToUPAndSystemQ.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0020_AddingLangToUPAndSystemQ.cs new file mode 100644 index 00000000..6d43f282 --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0020_AddingLangToUPAndSystemQ.cs @@ -0,0 +1,26 @@ +using FluentMigrator; + +namespace Resgrid.Providers.Migrations.Migrations +{ + [Migration(20)] + public class M0020_AddingLangToUPAndSystemQ : Migration + { + public override void Up() + { + // Adding Language to User Profile + Alter.Table("UserProfiles").AddColumn("Language").AsString().Nullable(); + + // Updating Queue Item to handle System Queue stuff (i.e. Delete Account and Department requests) + Alter.Table("QueueItems").AddColumn("ToBeCompletedOn").AsDateTime2().Nullable(); + Alter.Table("QueueItems").AddColumn("Reason").AsString().Nullable(); + Alter.Table("QueueItems").AddColumn("QueuedByUserId").AsString(128).Nullable(); + Alter.Table("QueueItems").AddColumn("Data").AsString().Nullable(); + Alter.Table("QueueItems").AddColumn("ReminderCount").AsInt32().Nullable(); + } + + public override void Down() + { + + } + } +} diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0021_AddingCallReferences.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0021_AddingCallReferences.cs new file mode 100644 index 00000000..2ef88c2f --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0021_AddingCallReferences.cs @@ -0,0 +1,33 @@ +using FluentMigrator; +using System; + +namespace Resgrid.Providers.Migrations.Migrations +{ + [Migration(21)] + public class M0021_AddingCallReferences : Migration + { + public override void Up() + { + Create.Table("CallReferences") + .WithColumn("CallReferenceId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("SourceCallId").AsInt32().NotNullable() + .WithColumn("TargetCallId").AsInt32().NotNullable() + .WithColumn("AddedOn").AsDateTime2().NotNullable() + .WithColumn("AddedByUserId").AsString(128).NotNullable() + .WithColumn("Note").AsString().Nullable(); + + Create.ForeignKey("FK_CallReferences_Call_Source") + .FromTable("CallReferences").ForeignColumn("SourceCallId") + .ToTable("Calls").PrimaryColumn("CallId"); + + Create.ForeignKey("FK_CallReferences_Call_Target") + .FromTable("CallReferences").ForeignColumn("TargetCallId") + .ToTable("Calls").PrimaryColumn("CallId"); + } + + public override void Down() + { + + } + } +} diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0022_AddingQuantityForPlanAddon.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0022_AddingQuantityForPlanAddon.cs new file mode 100644 index 00000000..e1fb0b9e --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0022_AddingQuantityForPlanAddon.cs @@ -0,0 +1,19 @@ +using FluentMigrator; +using System; + +namespace Resgrid.Providers.Migrations.Migrations +{ + [Migration(22)] + public class M0022_AddingQuantityForPlanAddon : Migration + { + public override void Up() + { + Alter.Table("PaymentAddons").AddColumn("Quantity").AsInt64().NotNullable().WithDefaultValue(1); + } + + public override void Down() + { + + } + } +} diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0023_AddingPlanAddonFor10Pack.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0023_AddingPlanAddonFor10Pack.cs new file mode 100644 index 00000000..63c5c8f8 --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0023_AddingPlanAddonFor10Pack.cs @@ -0,0 +1,26 @@ +using FluentMigrator; +using System; + +namespace Resgrid.Providers.Migrations.Migrations +{ + [Migration(23)] + public class M0023_AddingPlanAddonFor10Pack : Migration + { + public override void Up() + { + if (Schema.Table("PlanAddons").Constraint("FK_PlanAddons_Plans").Exists()) + { + Delete.ForeignKey("FK_PlanAddons_Plans").OnTable("PlanAddons"); + } + + Alter.Table("PlanAddons").AlterColumn("PlanId").AsInt32().Nullable(); + Alter.Table("PlanAddons").AddColumn("TestExternalId").AsString(256).Nullable(); + Insert.IntoTable("PlanAddons").Row(new { PlanAddonId = "6f4c5f8b-584d-4291-8a7d-29bf97ae6aa9", AddonType = 1, Cost = 0, ExternalId = "NA", TestExternalId = "NA" }); + } + + public override void Down() + { + + } + } +} diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0024_AddingDepartmentAudio.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0024_AddingDepartmentAudio.cs new file mode 100644 index 00000000..34cdde28 --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0024_AddingDepartmentAudio.cs @@ -0,0 +1,30 @@ +using FluentMigrator; +using System; + +namespace Resgrid.Providers.Migrations.Migrations +{ + [Migration(24)] + public class M0024_AddingDepartmentAudio : Migration + { + public override void Up() + { + Create.Table("DepartmentAudios") + .WithColumn("DepartmentAudioId").AsString(128).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("DepartmentAudioType").AsInt32().NotNullable() + .WithColumn("Name").AsString().Nullable() + .WithColumn("Data").AsString().Nullable() + .WithColumn("AddedOn").AsDateTime2().NotNullable() + .WithColumn("AddedByUserId").AsString(128).NotNullable(); + + Create.ForeignKey("FK_DepartmentAudios_Department") + .FromTable("DepartmentAudios").ForeignColumn("DepartmentId") + .ToTable("Departments").PrimaryColumn("DepartmentId"); + } + + public override void Down() + { + + } + } +} diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0025_AddingTypeToDepartmentAudio.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0025_AddingTypeToDepartmentAudio.cs new file mode 100644 index 00000000..00f86846 --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0025_AddingTypeToDepartmentAudio.cs @@ -0,0 +1,19 @@ +using FluentMigrator; +using System; + +namespace Resgrid.Providers.Migrations.Migrations +{ + [Migration(25)] + public class M0025_AddingTypeToDepartmentAudio : Migration + { + public override void Up() + { + Alter.Table("DepartmentAudios").AddColumn("Type").AsString(256).Nullable(); + } + + public override void Down() + { + + } + } +} diff --git a/Providers/Resgrid.Providers.Number/TextMessageProvider.cs b/Providers/Resgrid.Providers.Number/TextMessageProvider.cs index 49e65949..8f9f601b 100644 --- a/Providers/Resgrid.Providers.Number/TextMessageProvider.cs +++ b/Providers/Resgrid.Providers.Number/TextMessageProvider.cs @@ -28,6 +28,23 @@ public class TextMessageProvider : ITextMessageProvider public async Task SendTextMessage(string number, string message, string departmentNumber, MobileCarriers carrier, int departmentId, bool forceGateway = false, bool isCall = false) { + var wasTwillioSuccessful = await SendTextMessageViaTwillio(number, message, departmentNumber); + + if (!wasTwillioSuccessful && isCall) + { + return await SendTextMessageViaSignalWire(number, message, departmentNumber); + } + + return wasTwillioSuccessful; + + /* I'm leaving the abomination below as a comment just in case I need to quickly reference the code block for any missing + * edge cases at 1AM Dealing with a production issue. The below is the result of trying to give as much functionality + * to the free and low paying tiers as possible by lowering Resgrid's costs. But the Greedflation of the 2020's has + * killed my ability to stem the tide anymore. Signalwire will still be used but now only as a backup for calls only. + * Free tiers don't do SMS anymore at all and Standard and Permium tiers no longer can use Message based SMS. -SJ 6-20-2023. + */ + + /* if (carrier == MobileCarriers.Telstra) { return await SendTextMessageViaNexmo(number, message, departmentNumber); @@ -99,6 +116,7 @@ public async Task SendTextMessage(string number, string message, string de return await SendTextMessageViaSignalWire(number, message, departmentNumber); } } + */ } private async Task SendTextMessageViaNexmo(string number, string message, string departmentNumber) @@ -241,19 +259,27 @@ public string GetSendingPhoneNumber(string number) { var sendingPhoneNumber = Config.NumberProviderConfig.SignalWireResgridNumber; - try - { - var zone = GetZoneForAreaCode(number); - var possibleNumbers = Config.NumberProviderConfig.SignalWireZones[zone]; - var sendingNumber = possibleNumbers.Random(); - - if (!String.IsNullOrWhiteSpace(sendingNumber)) - sendingPhoneNumber = sendingNumber; - } - catch (Exception ex) - { - Logging.LogException(ex); - } + /* + * Logic below was to work around some issues sending messages to numbers via + * Signalwire, it looks like they would 'spam' block or rate limit if all sent + * from one number. So added regions and message would be sent to users from a + * Region their number is located in, also helped them get stuff from a local'ish + * number. But it makes sense to just migrate this to toll-free numbers now. -SJ + * + */ + //try + //{ + // var zone = GetZoneForAreaCode(number); + // var possibleNumbers = Config.NumberProviderConfig.SignalWireZones[zone]; + // var sendingNumber = possibleNumbers.Random(); + + // if (!String.IsNullOrWhiteSpace(sendingNumber)) + // sendingPhoneNumber = sendingNumber; + //} + //catch (Exception ex) + //{ + // Logging.LogException(ex); + //} return sendingPhoneNumber; } diff --git a/Providers/Resgrid.Providers.Voip/LiveKit/LiveKitAccessToken.cs b/Providers/Resgrid.Providers.Voip/LiveKit/LiveKitAccessToken.cs new file mode 100644 index 00000000..34d876ac --- /dev/null +++ b/Providers/Resgrid.Providers.Voip/LiveKit/LiveKitAccessToken.cs @@ -0,0 +1,81 @@ +using System; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Principal; +using System.Linq; +using Resgrid.Config; + +namespace Resgrid.Providers.Voip.LiveKit +{ + public class LiveKitAccessToken + { + private LiveKitGrant grants; + private string identity; + private double minutesForClaimTtl; + + /// + /// AccessToken + /// + /// API key + /// API secret + /// Grants to include as claims + public LiveKitAccessToken(LiveKitGrant grants, double minutesForClaimTtl = 5, string identity = null) + { + this.grants = grants; + this.identity = identity; + this.minutesForClaimTtl = minutesForClaimTtl; + } + + /// + /// Generates a new JWT token with 1 day validity + /// + /// A JWT token string + public string GetToken(string room = null) + { + if (room != null) + { + grants.video.room = room; + } + + var tokenDescriptor = new SecurityTokenDescriptor + { + Claims = grants.ToDictionary(), + Expires = DateTime.UtcNow.AddMinutes(minutesForClaimTtl), + Issuer = VoipConfig.LiveKitServerApiKey, + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(VoipConfig.LiveKitServerApiSecret)), SecurityAlgorithms.HmacSha256Signature) + }; + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor); + + if (this.identity != null) + { + token.Payload.Add("sub", this.identity); + } + + return tokenHandler.WriteToken(token); + } + + /// + /// Verify a given signed token with issuer and lifetime validation + /// + /// A signed token + /// SHA256 Claim included in the token + public string VerifyToken(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var parameters = new TokenValidationParameters() + { + ValidateLifetime = true, + ValidateAudience = false, + ValidateIssuer = true, + ValidIssuer = VoipConfig.LiveKitServerApiKey, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(VoipConfig.LiveKitServerApiSecret)) + }; + SecurityToken validated; + IPrincipal principal = tokenHandler.ValidateToken(token, parameters, out validated); + + return ((JwtSecurityToken)validated).Claims.First(x => x.Type == "sha256").Value; + } + } +} diff --git a/Providers/Resgrid.Providers.Voip/LiveKit/LiveKitGrant.cs b/Providers/Resgrid.Providers.Voip/LiveKit/LiveKitGrant.cs new file mode 100644 index 00000000..708a1640 --- /dev/null +++ b/Providers/Resgrid.Providers.Voip/LiveKit/LiveKitGrant.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Providers.Voip.LiveKit +{ + public class LiveKitGrant + { + public string identity { get; set; } + public LiveKitVideoGrant video { get; set; } + public string metadata { get; set; } + + public IDictionary ToDictionary() + { + var dir = new Dictionary(); + dir.Add("identity", identity); + dir.Add("video", video); + dir.Add("metadata", metadata); + return dir; + } + } +} diff --git a/Providers/Resgrid.Providers.Voip/LiveKit/LiveKitVideoGrant.cs b/Providers/Resgrid.Providers.Voip/LiveKit/LiveKitVideoGrant.cs new file mode 100644 index 00000000..fcd75ed4 --- /dev/null +++ b/Providers/Resgrid.Providers.Voip/LiveKit/LiveKitVideoGrant.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Providers.Voip.LiveKit +{ + public class LiveKitVideoGrant + { + public bool roomCreate { get; set; } + public bool roomList { get; set; } + public bool roomRecord { get; set; } + + public bool roomAdmin { get; set; } + public bool roomJoin { get; set; } + public string room { get; set; } + + public bool? canPublish { get; set; } + public bool? canSubscribe { get; set; } + public bool? canPublishData { get; set; } + + public bool? hidden { get; set; } + } +} diff --git a/Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitAccessTokenOptions.cs b/Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitAccessTokenOptions.cs new file mode 100644 index 00000000..c260bc51 --- /dev/null +++ b/Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitAccessTokenOptions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Resgrid.Providers.Voip.LiveKit.Model +{ + internal class LiveKitAccessTokenOptions + { + /** + * amount of time before expiration + * expressed in seconds or a string describing a time span zeit/ms. + * eg: '2 days', '10h', or seconds as numeric value + */ + public string ttl { get; set; } + + /** + * display name for the participant, available as `Participant.name` + */ + public string name { get; set; } + + /** + * identity of the user, required for room join tokens + */ + public string identity { get; set; } + + /** + * custom metadata to be passed to participants + */ + public string metadata { get; set; } + } +} diff --git a/Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitParticipantInfo.cs b/Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitParticipantInfo.cs new file mode 100644 index 00000000..0a35894d --- /dev/null +++ b/Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitParticipantInfo.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; + +namespace Resgrid.Providers.Voip.LiveKit.Model +{ + /// + /// https://docs.livekit.io/server/room-management/#participantinfo + /// + public class LiveKitParticipantInfo + { + [JsonProperty("sid")] + public string Sid { get; set; } + + [JsonProperty("identity")] + public string Identity { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("numberOfElements")] + public int NumberOfElements { get; set; } + + //[JsonProperty("tracks")] + //public int Tracks { get; set; } + + [JsonProperty("metadata")] + public string Metadata { get; set; } + + [JsonProperty("joined_at")] + public long JoinedAt { get; set; } + + //[JsonProperty("permission")] + //public int Permission { get; set; } + + [JsonProperty("is_publisher")] + public bool IsPublisher { get; set; } + } +} diff --git a/Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitParticipants.cs b/Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitParticipants.cs new file mode 100644 index 00000000..6e292de5 --- /dev/null +++ b/Providers/Resgrid.Providers.Voip/LiveKit/Model/LiveKitParticipants.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Resgrid.Providers.Voip.LiveKit.Model +{ + /// + /// https://docs.livekit.io/server/room-management/#participantinfo + /// + public class LiveKitParticipants + { + [JsonProperty("participants")] + public List Participants { get; set; } + } +} diff --git a/Providers/Resgrid.Providers.Voip/LiveKitProvider.cs b/Providers/Resgrid.Providers.Voip/LiveKitProvider.cs new file mode 100644 index 00000000..e05d353d --- /dev/null +++ b/Providers/Resgrid.Providers.Voip/LiveKitProvider.cs @@ -0,0 +1,63 @@ +using Resgrid.Providers.Voip.LiveKit; +using Resgrid.Providers.Voip.LiveKit.Model; +using RestSharp; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; + +namespace Resgrid.Providers.Voip +{ + public class LiveKitProvider + { + public string GetTokenForRoom(string name, string roomName) + { + var ac = new LiveKitAccessToken(new LiveKitGrant() + { + video = new LiveKitVideoGrant() + { + roomJoin = true, + room = roomName + } + }, 1440, name); + + return ac.GetToken(); + } + + + + public async Task> ListRoomParticipants(string roomName) + { + var client = new RestClient(Config.VoipConfig.LiveKitServerApiUrl); + + var ac = new LiveKitAccessToken(new LiveKitGrant() + { + video = new LiveKitVideoGrant() + { + roomJoin = true, + roomAdmin = true, + room = roomName + } + }); + + var request = new RestRequest($"/twirp/livekit.RoomService/ListParticipants", Method.Post); + request.AddHeader("Authorization", "Bearer " + ac.GetToken()); + request.AddHeader("Content-Type", "application/json"); + + var body = new + { + room = roomName + }; + request.AddJsonBody(body); + + var response = await client.ExecuteAsync(request); + + if (response.StatusCode == HttpStatusCode.NotFound) + return new List(); // Room is not active + + if (response == null || response.Data == null) + return new List(); + + return response.Data.Participants; + } + } +} diff --git a/Providers/Resgrid.Providers.Voip/Resgrid.Providers.Voip.csproj b/Providers/Resgrid.Providers.Voip/Resgrid.Providers.Voip.csproj index e31fcbd3..54f6fda7 100644 --- a/Providers/Resgrid.Providers.Voip/Resgrid.Providers.Voip.csproj +++ b/Providers/Resgrid.Providers.Voip/Resgrid.Providers.Voip.csproj @@ -7,6 +7,12 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Repositories/Resgrid.Repositories.DataRepository/CallReferencesRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CallReferencesRepository.cs new file mode 100644 index 00000000..138ef2c8 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/CallReferencesRepository.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +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.Calls; +using Resgrid.Repositories.DataRepository.Queries.Workshifts; + +namespace Resgrid.Repositories.DataRepository +{ + public class CallReferencesRepository : RepositoryBase, ICallReferencesRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public CallReferencesRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + public async Task> GetCallReferencesByTargetCallIdAsync(int callId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParameters(); + dynamicParameters.Add("CallId", callId); + + var query = _queryFactory.GetQuery(); + + var result = await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction, + map: (cr, c) => { cr.SourceCall = c; return cr; }, + splitOn: "CallId"); + + return result; + }); + + 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); + + return null; + } + } + + public async Task> GetCallReferencesBySourceCallIdAsync(int callId) + { + try + { + var selectFunction = new Func>>(async x => + { + var dynamicParameters = new DynamicParameters(); + dynamicParameters.Add("CallId", callId); + + var query = _queryFactory.GetQuery(); + + var result = await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction, + map: (cr, c) => { cr.TargetCall = c; return cr; }, + splitOn: "CallId"); + + return result; + }); + + 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); + + return null; + } + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs index 655da42f..b30de988 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs @@ -411,6 +411,25 @@ protected SqlConfiguration() { } public string SelectUnitStatesByUnitInDateRangeQuery { get; set; } #endregion Unit States + #region Workshifts + public string WorkshiftsTable { get; set; } + public string WorkshiftDaysTable { get; set; } + public string WorkshiftEntitiesTable { get; set; } + public string WorkshiftFillsTable { get; set; } + public string SelectAllWorkshiftsAndDaysByDidQuery { get; set; } + public string SelectWorkshiftByIdQuery { get; set; } + public string SelectWorkshiftEntitiesByWorkshiftIdQuery { get; set; } + public string SelectWorkshiftFillsByWorkshiftIdQuery { get; set; } + #endregion Workshifts + + #region CallReferences + public string CallReferencesTable { get; set; } + public string SelectAllCallReferencesBySourceCallIdQuery { get; set; } + public string SelectAllCallReferencesByTargetCallIdQuery { get; set; } + #endregion CallReferences + + + // Identity #region Table Names @@ -455,16 +474,5 @@ protected SqlConfiguration() { } public string RemoveLoginForUserQuery { get; set; } public string UpdateClaimForUserQuery { get; set; } #endregion - - #region Workshifts - public string WorkshiftsTable { get; set; } - public string WorkshiftDaysTable { get; set; } - public string WorkshiftEntitiesTable { get; set; } - public string WorkshiftFillsTable { get; set; } - public string SelectAllWorkshiftsAndDaysByDidQuery { get; set; } - public string SelectWorkshiftByIdQuery { get; set; } - public string SelectWorkshiftEntitiesByWorkshiftIdQuery { get; set; } - public string SelectWorkshiftFillsByWorkshiftIdQuery { get; set; } - #endregion Workshifts } } diff --git a/Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs new file mode 100644 index 00000000..62669347 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/DeleteRepository.cs @@ -0,0 +1,179 @@ +using System; +using System.Configuration; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Dapper; +using Dapper.Contrib.Extensions; +using Resgrid.Model.Identity; +using Resgrid.Model.Repositories; +using Resgrid.Model; +using Resgrid.Config; + +namespace Resgrid.Repositories.DataRepository +{ + public class DeleteRepository: IDeleteRepository + { + public async Task DeleteDepartmentAndUsersAsync(int departmentId) + { + Dapper.SqlMapper.Settings.CommandTimeout = 300; + + using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["ResgridContext"].ConnectionString)) + { + using (var transaction = db.BeginTransaction()) + { + var result = await db.ExecuteAsync(@" + DECLARE @UserId NVARCHAR(128) + DECLARE @UnitId INT + DECLARE @ManagingUserId NVARCHAR(128) + + SET @ManagingUserId = (SELECT ManagingUserId FROM [dbo].[Departments] WHERE DepartmentId = @DepartmentId) + + DECLARE db_cursor CURSOR FOR + SELECT UserId + FROM [dbo].[DepartmentMembers] + WHERE DepartmentId = @DepartmentId AND UserId != @ManagingUserId + + DECLARE unit_cursor CURSOR FOR + SELECT UnitId + FROM [dbo].[Units] + WHERE DepartmentId = @DepartmentId + + OPEN db_cursor + FETCH NEXT FROM db_cursor INTO @UserId + + -- Clear all the users out in the department + WHILE @@FETCH_STATUS = 0 + BEGIN + DELETE FROM [dbo].[ScheduledTasks] WHERE UserId = @UserId AND DepartmentId = @DepartmentId + DELETE FROM [dbo].[UserStates] WHERE UserId = @UserId AND DepartmentId = @DepartmentId + DELETE FROM [dbo].[Logs] WHERE LoggedByUserId = @UserId AND DepartmentId = @DepartmentId + DELETE FROM [dbo].[MessageRecipients] WHERE UserId = @UserId + DELETE FROM [dbo].[MessageRecipients] WHERE UserId = @UserId + DELETE FROM [dbo].[MessageRecipients] WHERE MessageId IN (SELECT MessageId FROM [dbo].[Messages] WHERE ReceivingUserId = @UserId) + DELETE FROM [dbo].[MessageRecipients] WHERE MessageId IN (SELECT MessageId FROM [dbo].[Messages] WHERE SendingUserId = @UserId) + DELETE FROM [dbo].[Messages] WHERE SendingUserId = @UserId + DELETE FROM [dbo].[Messages] WHERE ReceivingUserId = @UserId + DELETE FROM [dbo].[PersonnelCertifications] WHERE UserId = @UserId AND DepartmentId = @DepartmentId + DELETE FROM [dbo].[PersonnelRoleUsers] WHERE UserId = @UserId AND DepartmentId = @DepartmentId + DELETE FROM [dbo].[PushUris] WHERE UserId = @UserId + DELETE FROM [dbo].[UserStates] WHERE UserId = @UserId AND DepartmentId = @DepartmentId + DELETE FROM [dbo].[ActionLogs] WHERE UserId = @UserId AND DepartmentId = @DepartmentId + DELETE FROM [dbo].[DepartmentMembers] WHERE UserId = @UserId AND DepartmentId = @DepartmentId + DELETE FROM [dbo].[DepartmentGroupMembers] WHERE UserId = @UserId AND DepartmentId = @DepartmentId + DELETE FROM [dbo].[DistributionListMembers] WHERE UserId = @UserId + DELETE FROM [dbo].[PersonnelRoleUsers] WHERE UserId = @UserId AND DepartmentId = @DepartmentId + DELETE FROM [dbo].[UnitStateRoles] WHERE UserId = @UserId --AND DepartmentId = @DepartmentId + DELETE FROM [dbo].[CallDispatches] WHERE UserId = @UserId --AND DepartmentId = @DepartmentId + + IF (SELECT COUNT(*) FROM DepartmentMembers WHERE UserId = @UserId) = 1 + BEGIN + -- This user is only a member of one department so clear their account out as well + DELETE FROM [dbo].[UserProfiles] WHERE UserId = @UserId + DELETE FROM [dbo].[AspNetUserClaims] WHERE UserId = @UserId + DELETE FROM [dbo].[AspNetUserLogins] WHERE UserId = @UserId + DELETE FROM [dbo].[AspNetUserRoles] WHERE UserId = @UserId + DELETE FROM [dbo].[AspNetUsersExt] WHERE UserId = @UserId + DELETE FROM [dbo].[AspNetUsers] WHERE Id = @UserId + END + + FETCH NEXT FROM db_cursor INTO @UserId + END + + CLOSE db_cursor + DEALLOCATE db_cursor + + OPEN unit_cursor + FETCH NEXT FROM unit_cursor INTO @UnitId + + -- Clear all the unit out in the department + WHILE @@FETCH_STATUS = 0 + BEGIN + DELETE FROM [dbo].[UnitLogs] WHERE UnitId = @UnitId + DELETE FROM [dbo].[UnitActiveRoles] WHERE UnitId = @UnitId + DELETE FROM [dbo].[UnitRoles] WHERE UnitId = @UnitId + DELETE FROM [dbo].[UnitStates] WHERE UnitId = @UnitId + DELETE FROM [dbo].[Inventories] WHERE UnitId = @UnitId + DELETE FROM [dbo].[LogUsers] WHERE UnitId = @UnitId + DELETE FROM [dbo].[Units] WHERE UnitId = @UnitId + + FETCH NEXT FROM unit_cursor INTO @UnitId + END + + CLOSE unit_cursor + DEALLOCATE unit_cursor + + -- Delete all the department level data + DELETE FROM [dbo].[Invites] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[Payments] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[ActionLogs] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[Inventories] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[InventoryTypes] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[Logs] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[Calls] WHERE DepartmentId = @DepartmentId + --DELETE FROM [dbo].[Addresses] WHERE AddressId = (SELECT AddressId FROM [dbo].[Departments] WHERE DepartmentId = @DepartmentId) + --DELETE FROM [dbo].[Addresses] WHERE AddressId IN (SELECT AddressId FROM [dbo].[DepartmentGroups] WHERE DepartmentId = @DepartmentId) + DELETE FROM [dbo].[DepartmentGroupMembers] WHERE DepartmentGroupId IN (SELECT DepartmentGroupId FROM [dbo].[DepartmentGroups] WHERE DepartmentId = @DepartmentId) + DELETE FROM [dbo].[Shifts] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[DepartmentGroups] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[DepartmentSettings] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[DistributionLists] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[Documents] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[Invites] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[Notes] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[Payments] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[PersonnelRoles] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[CommandDefinitions] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[UnitTypes] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[DispatchProtocols] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[Forms] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[PaymentAddons] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[ResourceOrders] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[Trainings] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[DepartmentLinks] WHERE LinkedDepartmentId = @DepartmentId + DELETE FROM [dbo].[DepartmentLinks] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[DepartmentVoiceChannels] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[DepartmentVoices] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[CallQuickTemplates] WHERE DepartmentId = @DepartmentId + DELETE FROM [dbo].[Departments] WHERE DepartmentId = @DepartmentId + + -- Delete the managing member's user + DELETE FROM [dbo].[ScheduledTasks] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[UserStates] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[Logs] WHERE LoggedByUserId = @ManagingUserId + DELETE FROM [dbo].[MessageRecipients] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[MessageRecipients] WHERE MessageId IN (SELECT MessageId FROM [dbo].[Messages] WHERE ReceivingUserId = @ManagingUserId) + DELETE FROM [dbo].[MessageRecipients] WHERE MessageId IN (SELECT MessageId FROM [dbo].[Messages] WHERE SendingUserId = @ManagingUserId) + DELETE FROM [dbo].[Messages] WHERE ReceivingUserId = @ManagingUserId + DELETE FROM [dbo].[Messages] WHERE SendingUserId = @ManagingUserId + DELETE FROM [dbo].[PersonnelCertifications] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[PersonnelRoleUsers] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[PushUris] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[UserProfiles] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[UserStates] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[ActionLogs] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[DepartmentMembers] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[DepartmentGroupMembers] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[DistributionListMembers] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[PersonnelRoleUsers] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[PushUris] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[UnitStateRoles] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[CallDispatches] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[AspNetUserClaims] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[AspNetUserLogins] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[AspNetUserRoles] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[AspNetUsersExt] WHERE UserId = @ManagingUserId + DELETE FROM [dbo].[AspNetUsers] WHERE Id = @ManagingUserId + ", + new { DepartmentId = departmentId }); + + transaction.Commit(); + } + } + + return false; + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/DepartmentAudioRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/DepartmentAudioRepository.cs new file mode 100644 index 00000000..6eca267d --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/DepartmentAudioRepository.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using System.Threading.Tasks; +using Dapper; +using Resgrid.Framework; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Queries.Voice; + +namespace Resgrid.Repositories.DataRepository +{ + public class DepartmentAudioRepository : RepositoryBase, IDepartmentAudioRepository + { + private readonly IConnectionProvider _connectionProvider; + private readonly SqlConfiguration _sqlConfiguration; + private readonly IQueryFactory _queryFactory; + private readonly IUnitOfWork _unitOfWork; + + public DepartmentAudioRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _connectionProvider = connectionProvider; + _sqlConfiguration = sqlConfiguration; + _queryFactory = queryFactory; + _unitOfWork = unitOfWork; + } + + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs index 87537ed3..bb66202c 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/IdentityRepository.cs @@ -270,5 +270,31 @@ public async Task CleanUpOIDCTokensAsync(DateTime timestamp) return false; } + + public async Task ClearOutUserLoginAsync(string userId) + { + using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["ResgridContext"].ConnectionString)) + { + var result = await db.ExecuteAsync(@"UPDATE AspNetUsers + SET UserName = @deleteid, + Email = @deleteid + '@resgrid.del' + WHERE Id = @userId", + new { userId = userId, deleteid = Guid.NewGuid().ToString() }); + } + + return false; + } + + public async Task CleanUpOIDCTokensByUserAsync(string userId) + { + using (IDbConnection db = new SqlConnection(OidcConfig.ConnectionString)) + { + var result = await db.ExecuteAsync(@"DELETE FROM OpenIddictTokens + WHERE Subject = @userId", + new { userId = userId }); + } + + return false; + } } } diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs index 4bec3b71..ec6d4ee8 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs @@ -145,6 +145,9 @@ 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(); } } } diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs index f39a3d1b..cc96234f 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs @@ -145,6 +145,9 @@ 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(); } } } diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs index 41e5a12c..56677c9b 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs @@ -145,6 +145,9 @@ 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(); } } } diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs index 7bcdfd41..33bfb970 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs @@ -145,6 +145,9 @@ 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(); } } } diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Calls/SelectAllCallReferencesBySourceCallIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calls/SelectAllCallReferencesBySourceCallIdQuery.cs new file mode 100644 index 00000000..bda3d500 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calls/SelectAllCallReferencesBySourceCallIdQuery.cs @@ -0,0 +1,46 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Calls +{ + public class SelectAllCallReferencesBySourceCallIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectAllCallReferencesBySourceCallIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectAllCallReferencesBySourceCallIdQuery + .ReplaceQueryParameters(_sqlConfiguration.SchemaName, + string.Empty, + _sqlConfiguration.ParameterNotation, + new string[] { + "%CALLID%" + }, + new string[] { + "CallId", + }, + new string[] { + "%CALLREFERENCESTABLE%", + "%CALLSTABLE%" + }, + new string[] { + _sqlConfiguration.CallReferencesTable, + _sqlConfiguration.CallsTable + } + ); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/Calls/SelectAllCallReferencesByTargetCallIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calls/SelectAllCallReferencesByTargetCallIdQuery.cs new file mode 100644 index 00000000..b7da365f --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/Calls/SelectAllCallReferencesByTargetCallIdQuery.cs @@ -0,0 +1,46 @@ +using Resgrid.Model; +using Resgrid.Model.Repositories.Queries.Contracts; +using Resgrid.Repositories.DataRepository.Configs; +using Resgrid.Repositories.DataRepository.Extensions; + +namespace Resgrid.Repositories.DataRepository.Queries.Calls +{ + public class SelectAllCallReferencesByTargetCallIdQuery : ISelectQuery + { + private readonly SqlConfiguration _sqlConfiguration; + public SelectAllCallReferencesByTargetCallIdQuery(SqlConfiguration sqlConfiguration) + { + _sqlConfiguration = sqlConfiguration; + } + + public string GetQuery() + { + var query = _sqlConfiguration.SelectAllCallReferencesByTargetCallIdQuery + .ReplaceQueryParameters(_sqlConfiguration.SchemaName, + string.Empty, + _sqlConfiguration.ParameterNotation, + new string[] { + "%CALLID%" + }, + new string[] { + "CallId", + }, + new string[] { + "%CALLREFERENCESTABLE%", + "%CALLSTABLE%" + }, + new string[] { + _sqlConfiguration.CallReferencesTable, + _sqlConfiguration.CallsTable + } + ); + + return query; + } + + public string GetQuery() where TEntity : class, IEntity + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs index ceffabe1..0d1595a1 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs @@ -684,9 +684,9 @@ FROM UnitStates DeleteUnitActiveRolesByUnitIdQuery = @"DELETE FROM %SCHEMA%.%TABLENAME% WHERE [UnitId] = %UNITID%"; SelectActiveRolesForUnitsByDidQuery = @"SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [DepartmentId] = %DID%"; SelectUnitsByDIdQuery = @" - SELECT u.*, dg.* + SELECT u.*, ur.* FROM [dbo].[Units] u - LEFT JOIN [dbo].[DepartmentGroups] dg ON dg.[DepartmentGroupId] = u.[StationGroupId] + LEFT JOIN [dbo].[UnitRoles] ur ON ur.[UnitId] = u.[UnitId] WHERE u.[DepartmentId] = %DID%"; #endregion Units @@ -1304,6 +1304,20 @@ SELECT dvc.* FROM %SCHEMA%.%WORKSHIFTFILLSTABLE% WHERE [WorkshiftId] = %ID%"; #endregion Workshifts + + #region CallReferences + CallReferencesTable = "CallReferences"; + SelectAllCallReferencesBySourceCallIdQuery = @" + SELECT cr.*, c.* + FROM %SCHEMA%.%CALLREFERENCESTABLE% cr + INNER JOIN %SCHEMA%.%CALLSTABLE% c ON cr.TargetCallId = c.CallId + WHERE cr.[SourceCallId] = %CALLID%"; + SelectAllCallReferencesByTargetCallIdQuery = @" + SELECT cr.*, c.* + FROM %SCHEMA%.%CALLREFERENCESTABLE% cr + INNER JOIN %SCHEMA%.%CALLSTABLE% c ON cr.SourceCallId = c.CallId + WHERE cr.[TargetCallId] = %CALLID%"; + #endregion CallReferences } -} + } } diff --git a/Repositories/Resgrid.Repositories.DataRepository/UnitsRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/UnitsRepository.cs index 67bcbfbf..a8892e72 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/UnitsRepository.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/UnitsRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data.Common; +using System.Linq; using System.Threading.Tasks; using Dapper; using Resgrid.Framework; @@ -125,6 +126,18 @@ public async Task> GetAllUnitsByDepartmentIdAsync(int departme var query = _queryFactory.GetQuery(); + var unitsDictionary = new Dictionary(); + var result = await x.QueryAsync(sql: query, + param: dynamicParameters, + transaction: _unitOfWork.Transaction, + map: UnitRolesMapping(unitsDictionary), + splitOn: "UnitRoleId"); + + if (unitsDictionary.Count > 0) + return unitsDictionary.Select(y => y.Value); + + return result; + return await x.QueryAsync(sql: query, param: dynamicParameters, transaction: _unitOfWork.Transaction, @@ -198,5 +211,40 @@ public async Task> GetAllUnitsForTypeAsync(int departmentId, s throw; } } + + private static Func UnitRolesMapping(Dictionary dictionary) + { + return new Func((unit, unitRole) => + { + var dictionaryUnit = default(Unit); + + if (unitRole != null) + { + if (dictionary.TryGetValue(unit.UnitId, out dictionaryUnit)) + { + if (dictionaryUnit.Roles.All(x => x.UnitRoleId != unitRole.UnitRoleId)) + dictionaryUnit.Roles.Add(unitRole); + } + else + { + if (unit.Roles == null) + unit.Roles = new List(); + + unit.Roles.Add(unitRole); + dictionary.Add(unit.UnitId, unit); + + dictionaryUnit = unit; + } + } + else + { + unit.Roles = new List(); + dictionaryUnit = unit; + dictionary.Add(unit.UnitId, unit); + } + + return dictionaryUnit; + }); + } } } diff --git a/ResgridCore.sln b/ResgridCore.sln index 22d89ef1..55c7ee2b 100644 --- a/ResgridCore.sln +++ b/ResgridCore.sln @@ -83,6 +83,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Resgrid.Providers.Voip", "P EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Resgrid.Repositories.NoSqlRepository", "Repositories\Resgrid.Repositories.NoSqlRepository\Resgrid.Repositories.NoSqlRepository.csproj", "{8C6EF4EA-DAF5-41A1-81A3-DFB0E085176D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Resgrid.Localization", "..\Resgrid\Core\Resgrid.Localization\Resgrid.Localization.csproj", "{E5462944-7B79-4247-9A16-22BE07F00684}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -495,6 +497,22 @@ Global {8C6EF4EA-DAF5-41A1-81A3-DFB0E085176D}.Staging|Any CPU.Build.0 = Debug|Any CPU {8C6EF4EA-DAF5-41A1-81A3-DFB0E085176D}.Staging|x86.ActiveCfg = Debug|Any CPU {8C6EF4EA-DAF5-41A1-81A3-DFB0E085176D}.Staging|x86.Build.0 = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Debug|x86.Build.0 = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Docker|Any CPU.ActiveCfg = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Docker|Any CPU.Build.0 = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Docker|x86.ActiveCfg = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Docker|x86.Build.0 = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Release|Any CPU.Build.0 = Release|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Release|x86.ActiveCfg = Release|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Release|x86.Build.0 = Release|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Staging|Any CPU.ActiveCfg = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Staging|Any CPU.Build.0 = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Staging|x86.ActiveCfg = Debug|Any CPU + {E5462944-7B79-4247-9A16-22BE07F00684}.Staging|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -527,11 +545,12 @@ Global {59E794E0-A247-4A7F-BCA1-DB928D59D876} = {D43D1D6B-66A9-4A57-9EA3-8DECC92FA583} {FA1E3331-62D2-478C-BF1E-412FF9E83562} = {F06D475C-635C-4DE4-82BA-C49A90BA8FCD} {8C6EF4EA-DAF5-41A1-81A3-DFB0E085176D} = {206D5D48-99B0-4913-B1E2-4BA11D021740} + {E5462944-7B79-4247-9A16-22BE07F00684} = {D43D1D6B-66A9-4A57-9EA3-8DECC92FA583} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35;packages\Unity.Interception.2.1.505.2\lib\NET35;packages\EnterpriseLibrary.Common.5.0.505.0\lib\NET35;packages\TransientFaultHandling.Core.5.1.1209.1\lib\NET4;packages\EnterpriseLibrary.WindowsAzure.TransientFaultHandling.5.1.1212.0\lib\NET4;packages\EnterpriseLibrary.WindowsAzure.TransientFaultHandling.5.1.1212.0\DesignTime - EnterpriseLibraryConfigurationToolBinariesPathV6 = packages\EnterpriseLibrary.Common.6.0.1304.0\lib\NET45 SolutionGuid = {156116FF-243E-45E8-8717-DB72E95F56AF} + EnterpriseLibraryConfigurationToolBinariesPathV6 = packages\EnterpriseLibrary.Common.6.0.1304.0\lib\NET45 + EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35;packages\Unity.Interception.2.1.505.2\lib\NET35;packages\EnterpriseLibrary.Common.5.0.505.0\lib\NET35;packages\TransientFaultHandling.Core.5.1.1209.1\lib\NET4;packages\EnterpriseLibrary.WindowsAzure.TransientFaultHandling.5.1.1212.0\lib\NET4;packages\EnterpriseLibrary.WindowsAzure.TransientFaultHandling.5.1.1212.0\DesignTime EndGlobalSection GlobalSection(CodealikeProperties) = postSolution SolutionGuid = 73e3ec62-fcea-43a4-b9ac-380e73a81ae7 diff --git a/Tests/Resgrid.Tests/Services/CommunicationServiceTests.cs b/Tests/Resgrid.Tests/Services/CommunicationServiceTests.cs index 5812508f..ef78d3e2 100644 --- a/Tests/Resgrid.Tests/Services/CommunicationServiceTests.cs +++ b/Tests/Resgrid.Tests/Services/CommunicationServiceTests.cs @@ -24,6 +24,8 @@ public class with_the_communication_service : TestBase protected Mock _outboundVoiceProviderMock; protected Mock _userProfileServiceMock; protected Mock _departmentSettingsServiceMock; + protected Mock _subscriptionsServiceMock; + protected Mock _userStateServiceMock; protected ICommunicationService _communicationService; @@ -36,9 +38,12 @@ protected with_the_communication_service() _outboundVoiceProviderMock = new Mock(); _userProfileServiceMock = new Mock(); _departmentSettingsServiceMock = new Mock(); + _subscriptionsServiceMock = new Mock(); + _userStateServiceMock = new Mock(); _communicationService = new CommunicationService(_smsServiceMock.Object, _emailServiceMock.Object, _pushServiceMock.Object, - _geoLocationProviderMock.Object, _outboundVoiceProviderMock.Object, _userProfileServiceMock.Object, _departmentSettingsServiceMock.Object); + _geoLocationProviderMock.Object, _outboundVoiceProviderMock.Object, _userProfileServiceMock.Object, _departmentSettingsServiceMock.Object, + _subscriptionsServiceMock.Object, _userStateServiceMock.Object); } } @@ -67,8 +72,11 @@ public async Task should_be_able_to_send_message() profile.SendMessagePush = true; profile.SendMessageSms = true; + Payment payment = new Payment(); + payment.PlanId = 2; + await _communicationService.SendMessageAsync(message, "Test Sender", "0000000", 1, profile); - _smsServiceMock.Verify(m => m.SendMessageAsync(message, "0000000", 1, profile)); + _smsServiceMock.Verify(m => m.SendMessageAsync(message, "0000000", 1, profile, payment)); _emailServiceMock.Verify(m => m.SendMessageAsync(message, "Test Sender", 1, profile, message.ReceivingUser)); _pushServiceMock.Verify(m => m.PushMessage(It.IsAny(), TestData.Users.TestUser1Id, profile)); } @@ -95,8 +103,11 @@ public async Task should_be_able_to_send_call() cd1.UserId = TestData.Users.TestUser2Id; call.Dispatches.Add(cd1); + Payment payment = new Payment(); + payment.PlanId = 2; + await _communicationService.SendCallAsync(call, cd, null, 1, null); - _smsServiceMock.Verify(m => m.SendCallAsync(call, cd, null, 1, null, null)); + _smsServiceMock.Verify(m => m.SendCallAsync(call, cd, null, 1, null, null, payment)); _emailServiceMock.Verify(m => m.SendCallAsync(call, cd, null)); //_pushServiceMock.Verify(m => m.PushCall(It.IsAny(), Users.TestUser1Id)); } diff --git a/Web/Resgrid.Web.Eventing/Startup.cs b/Web/Resgrid.Web.Eventing/Startup.cs index 0c71f284..a07293da 100644 --- a/Web/Resgrid.Web.Eventing/Startup.cs +++ b/Web/Resgrid.Web.Eventing/Startup.cs @@ -78,7 +78,7 @@ public void ConfigureServices(IServiceCollection services) bool configResult = ConfigProcessor.LoadAndProcessConfig(Configuration["AppOptions:ConfigPath"]); bool envConfigResult = ConfigProcessor.LoadAndProcessEnvVariables(Configuration.AsEnumerable()); - Framework.Logging.Initialize(ExternalErrorConfig.ExternalErrorServiceUrl); + Framework.Logging.Initialize(ExternalErrorConfig.ExternalErrorServiceUrlForEventing); var settings = System.Configuration.ConfigurationManager.ConnectionStrings; var element = typeof(ConfigurationElement).GetField("_readOnly", BindingFlags.Instance | BindingFlags.NonPublic); diff --git a/Web/Resgrid.Web.ServicesCore/Controllers/TwilioController.cs b/Web/Resgrid.Web.ServicesCore/Controllers/TwilioController.cs index 6704ad2b..1fada855 100644 --- a/Web/Resgrid.Web.ServicesCore/Controllers/TwilioController.cs +++ b/Web/Resgrid.Web.ServicesCore/Controllers/TwilioController.cs @@ -431,7 +431,7 @@ public async Task VoiceCall(string userId, int callId) { var response = new VoiceResponse(); var call = await _callsService.GetCallByIdAsync(callId); - call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true); + call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true, false); if (call == null) { diff --git a/Web/Resgrid.Web.ServicesCore/Controllers/v3/CallsController.cs b/Web/Resgrid.Web.ServicesCore/Controllers/v3/CallsController.cs index a0dbaa8f..0a8c9a35 100644 --- a/Web/Resgrid.Web.ServicesCore/Controllers/v3/CallsController.cs +++ b/Web/Resgrid.Web.ServicesCore/Controllers/v3/CallsController.cs @@ -383,7 +383,7 @@ public async Task>> GetActiveCallsForDep public async Task> GetCall(int callId) { var c = await _callsService.GetCallByIdAsync(callId); - c = await _callsService.PopulateCallData(c, false, true, true, false, false, false, false); + c = await _callsService.PopulateCallData(c, false, true, true, false, false, false, false, true); var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId, false); @@ -476,7 +476,7 @@ public async Task> GetCallExtraData(int callId) if (call.DepartmentId != DepartmentId) Unauthorized(); - call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true); + call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true, true); var groups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(DepartmentId); var units = await _unitsService.GetUnitsForDepartmentAsync(call.DepartmentId); @@ -759,7 +759,7 @@ public async Task SaveCall([FromBody] NewCallInput newCallInput, C if (!string.IsNullOrWhiteSpace(newCallInput.Geo)) call.GeoLocationData = newCallInput.Geo; - if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.Address) && call.GeoLocationData.Length > 1) + if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.Address)) call.GeoLocationData = await _geoLocationProvider.GetLatLonFromAddress(call.Address); if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.W3W)) @@ -964,7 +964,7 @@ public async Task> AddCall([FromBody] AddCallInput ca ReferenceNumber = callInput.ReferenceNumber }; - if (!string.IsNullOrWhiteSpace(call.GeoLocationData) && call.GeoLocationData.Length > 1 && !string.IsNullOrWhiteSpace(call.Address)) + if (!string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.Address)) call.GeoLocationData = await _geoLocationProvider.GetLatLonFromAddress(call.Address); if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.W3W)) @@ -1138,7 +1138,7 @@ public async Task EditCall([FromBody] EditCallInput editCallInput, { var call = await _callsService.GetCallByIdAsync(editCallInput.Cid); - call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true); + call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true, true); var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); if (call == null) @@ -1192,7 +1192,7 @@ public async Task EditCall([FromBody] EditCallInput editCallInput, if (!string.IsNullOrWhiteSpace(editCallInput.Geo)) call.GeoLocationData = editCallInput.Geo; - if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.Address) && call.GeoLocationData.Length > 1) + if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.Address)) call.GeoLocationData = await _geoLocationProvider.GetLatLonFromAddress(call.Address); if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.W3W)) @@ -1402,7 +1402,7 @@ public async Task>> GetCallNotes(int callId) if (call.DepartmentId != DepartmentId) return Unauthorized(); - call = await _callsService.PopulateCallData(call, false, false, true, false, false, false, false); + call = await _callsService.PopulateCallData(call, false, false, true, false, false, false, false, false); if (call.CallNotes == null || !call.CallNotes.Any()) return NotFound(); @@ -1453,7 +1453,7 @@ public async Task>> GetFilesForCall(int callId return Unauthorized(); var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); - call = await _callsService.PopulateCallData(call, false, true, false, false, false, false, false); + call = await _callsService.PopulateCallData(call, false, true, false, false, false, false, false, false); var baseUrl = Config.SystemBehaviorConfig.ResgridApiBaseUrl; diff --git a/Web/Resgrid.Web.ServicesCore/Controllers/v3/ChatController.cs b/Web/Resgrid.Web.ServicesCore/Controllers/v3/ChatController.cs index b9e19ecc..2c320f33 100644 --- a/Web/Resgrid.Web.ServicesCore/Controllers/v3/ChatController.cs +++ b/Web/Resgrid.Web.ServicesCore/Controllers/v3/ChatController.cs @@ -165,6 +165,7 @@ public async Task NotifyNewChat([FromBody] NotifyChatInput notifyC newChatEvent.RecipientUserIds = notifyChatInput.RecipientUserIds; newChatEvent.SendingUserId = notifyChatInput.SendingUserId; newChatEvent.Type = notifyChatInput.Type; + newChatEvent.DepartmentId = DepartmentId; CqrsEvent registerUnitPushEvent = new CqrsEvent(); registerUnitPushEvent.Type = (int)CqrsEventTypes.NewChatMessage; diff --git a/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallFilesController.cs b/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallFilesController.cs index 4c03a498..8f028ad8 100644 --- a/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallFilesController.cs +++ b/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallFilesController.cs @@ -64,7 +64,7 @@ public async Task> GetFilesForCall(int callId, boo return Unauthorized(); var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); - call = await _callsService.PopulateCallData(call, false, true, false, false, false, false, false); + call = await _callsService.PopulateCallData(call, false, true, false, false, false, false, false, false); if (call.Attachments != null && call.Attachments.Any()) { diff --git a/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallNotesController.cs b/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallNotesController.cs index 112821f2..1af49be6 100644 --- a/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallNotesController.cs +++ b/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallNotesController.cs @@ -65,7 +65,7 @@ public async Task> GetCallNotes(string callId) if (call.DepartmentId != DepartmentId) return Unauthorized(); - call = await _callsService.PopulateCallData(call, false, false, true, false, false, false, false); + call = await _callsService.PopulateCallData(call, false, false, true, false, false, false, false, false); if (call.CallNotes != null && call.CallNotes.Any()) { diff --git a/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallsController.cs b/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallsController.cs index adc2047f..6413445a 100644 --- a/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallsController.cs +++ b/Web/Resgrid.Web.ServicesCore/Controllers/v4/CallsController.cs @@ -105,7 +105,7 @@ public async Task> GetActiveCalls() { foreach (var c in calls) { - var callWithData = await _callsService.PopulateCallData(c, false, true, true, false, false, false, true); + var callWithData = await _callsService.PopulateCallData(c, false, true, true, false, false, false, true, true); string address = ""; if (String.IsNullOrWhiteSpace(c.Address) && c.HasValidGeolocationData()) @@ -161,7 +161,7 @@ public async Task> GetCall(string callId) if (!await _authorizationService.CanUserViewCallAsync(UserId, int.Parse(callId))) return Unauthorized(); - c = await _callsService.PopulateCallData(c, false, true, true, false, false, false, true); + c = await _callsService.PopulateCallData(c, false, true, true, false, false, false, true, true); string address = ""; if (String.IsNullOrWhiteSpace(c.Address) && c.HasValidGeolocationData()) @@ -220,7 +220,7 @@ public async Task> GetCallExtraData(int callId if (!await _authorizationService.CanUserViewCallAsync(UserId, callId)) return Unauthorized(); - call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true); + call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true, true); result.Data.CallFormData = call.CallFormData; @@ -568,7 +568,7 @@ public async Task> SaveCall([FromBody] NewCallInput if (!string.IsNullOrWhiteSpace(newCallInput.Geolocation)) call.GeoLocationData = newCallInput.Geolocation; - if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.Address) && call.GeoLocationData.Length > 1) + if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.Address)) call.GeoLocationData = await _geoLocationProvider.GetLatLonFromAddress(call.Address); if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.W3W)) @@ -829,7 +829,7 @@ public async Task> EditCall([FromBody] EditCallInpu var call = await _callsService.GetCallByIdAsync(int.Parse(editCallInput.Id)); - call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true); + call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true, true); var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); if (call == null) @@ -892,7 +892,7 @@ public async Task> EditCall([FromBody] EditCallInpu if (!string.IsNullOrWhiteSpace(editCallInput.Geolocation)) call.GeoLocationData = editCallInput.Geolocation; - if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.Address) && call.GeoLocationData.Length > 1) + if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.Address)) call.GeoLocationData = await _geoLocationProvider.GetLatLonFromAddress(call.Address); if (string.IsNullOrWhiteSpace(call.GeoLocationData) && !string.IsNullOrWhiteSpace(call.W3W)) @@ -1310,7 +1310,7 @@ public async Task> GetCallHistory(int callId) if (call.DepartmentId != DepartmentId) Unauthorized(); - call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true); + call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true, true); var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); result.Data.Add(new CallHistoryResultData() diff --git a/Web/Resgrid.Web.ServicesCore/Controllers/v4/ConfigController.cs b/Web/Resgrid.Web.ServicesCore/Controllers/v4/ConfigController.cs index 14f3db4f..1d963f65 100644 --- a/Web/Resgrid.Web.ServicesCore/Controllers/v4/ConfigController.cs +++ b/Web/Resgrid.Web.ServicesCore/Controllers/v4/ConfigController.cs @@ -50,6 +50,11 @@ public async Task> GetConfig(string key) { result.Data.MapUrl = MappingConfig.GetDispatchAppOSMUrl(); } + else if (key == InfoConfig.BigBoardKey) + { + result.Data.MapUrl = MappingConfig.GetBigBoardAppOSMUrl(); + result.Data.OpenWeatherApiKey = MappingConfig.BigBoardOpenWeatherApiKey; + } result.Data.MapAttribution = MappingConfig.LeafletAttribution; diff --git a/Web/Resgrid.Web.ServicesCore/Controllers/v4/PersonnelController.cs b/Web/Resgrid.Web.ServicesCore/Controllers/v4/PersonnelController.cs index 1aa82fbb..621ee7a4 100644 --- a/Web/Resgrid.Web.ServicesCore/Controllers/v4/PersonnelController.cs +++ b/Web/Resgrid.Web.ServicesCore/Controllers/v4/PersonnelController.cs @@ -198,10 +198,13 @@ public async Task> GetAllPersonnelInfos } else if (afilter.Substring(0, 2) == "U:") { - if (state != null && (state.State.ToString() == text || state.State.ToString() == text.Replace(" ", ""))) + if (state != null && !String.IsNullOrWhiteSpace(text)) { - result.Data.Add(s); - break; + if (state.State.ToString() == text || state.State.ToString() == text.Replace(" ", "")) + { + result.Data.Add(s); + break; + } } } diff --git a/Web/Resgrid.Web.ServicesCore/Controllers/v4/VoiceController.cs b/Web/Resgrid.Web.ServicesCore/Controllers/v4/VoiceController.cs index b7f3be19..28b5f07b 100644 --- a/Web/Resgrid.Web.ServicesCore/Controllers/v4/VoiceController.cs +++ b/Web/Resgrid.Web.ServicesCore/Controllers/v4/VoiceController.cs @@ -9,6 +9,9 @@ using System.Collections.Generic; using Resgrid.Web.Helpers; using Resgrid.Web.Services.Helpers; +using Microsoft.AspNetCore.Authorization; +using Resgrid.Providers.Voip; +using System.Web; namespace Resgrid.Web.Services.Controllers.v4 { @@ -24,15 +27,18 @@ public class VoiceController : V4AuthenticatedApiControllerbase private readonly IAuthorizationService _authorizationService; private readonly IVoiceService _voiceService; private readonly IDepartmentsService _departmentsService; + private readonly IUserProfileService _userProfileService; public VoiceController( IAuthorizationService authorizationService, IVoiceService voiceService, - IDepartmentsService departmentsService) + IDepartmentsService departmentsService, + IUserProfileService userProfileService) { _authorizationService = authorizationService; _voiceService = voiceService; _departmentsService = departmentsService; + _userProfileService = userProfileService; } #endregion Members and Constructors @@ -47,12 +53,16 @@ public async Task> GetDepartmentVoiceSetting var result = new DepartmentVoiceResult(); result.Data = new DepartmentVoiceResultData(); + var liveKitProvder = new LiveKitProvider(); + result.Data.VoipServerWebsocketSslAddress = Config.VoipConfig.VoipServerWebsocketSslAddress; result.Data.VoiceEnabled = await _voiceService.CanDepartmentUseVoiceAsync(DepartmentId); result.Data.Realm = Config.VoipConfig.VoipDomain; result.Data.CallerIdName = await UserHelper.GetFullNameForUser(UserId); result.Data.Type = (int)Config.SystemBehaviorConfig.VoipProviderType; + var userInfo = await _userProfileService.GetProfileByUserIdAsync(UserId); + if (result.Data.VoiceEnabled) { result.Data.Channels = new List(); @@ -71,12 +81,17 @@ public async Task> GetDepartmentVoiceSetting channel.IsDefault = chan.IsDefault; channel.ConferenceNumber = chan.ConferenceNumber; + if (Config.SystemBehaviorConfig.VoipProviderType == Config.VoipProviderTypes.LiveKit) + channel.Token = liveKitProvder.GetTokenForRoom(userInfo.FullName.AsFirstNameLastName, chan.DepartmentVoiceChannelId); + + result.Data.Channels.Add(channel); } } result.Data.UserInfo = new DepartmentVoiceUserInfoResultData(); result.Data.UserInfo.Username = UserId.Replace("-", ""); + result.Data.CanConnectApiToken = StringHelpers.Base64Encode(SymmetricEncryption.Encrypt(DepartmentId.ToString(), Config.SystemBehaviorConfig.ExternalLinkUrlParamPassphrase)); var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); result.Data.UserInfo.Pin = _departmentsService.ConvertDepartmentCodeToDigitPin(department.Code); @@ -120,5 +135,94 @@ public async Task> ConnectToSession(s return result; } + + /// + /// Determines if a user can connect to an voice session, limited to only LiveKit. + /// + /// Voice connection result containing the data needed to determine if a user can connect to a voice session + [HttpGet("CanConnectToVoiceSession")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CanConnectToVoiceSession(string token) + { + var result = new CanConnectToVoiceSessionResult(); + result.PageSize = 0; + result.Status = ResponseHelper.NotFound; + result.Data = new CanConnectToVoiceSessionResultData(); + + if (string.IsNullOrWhiteSpace(token)) + return result; + + string depId = null; + try + { + depId = SymmetricEncryption.Decrypt(StringHelpers.Base64Decode(token), Config.SystemBehaviorConfig.ExternalLinkUrlParamPassphrase); + } + catch { } + + if (string.IsNullOrWhiteSpace(depId)) + return result; + + // Disabling for now, as there is some converstion between + //if (Config.SystemBehaviorConfig.VoipProviderType == Config.VoipProviderTypes.LiveKit) + //{ + var info = await _voiceService.GetCurrentUtilizationForLiveKit(int.Parse(depId)); + + if (info != null) + { + result.Data.CurrentSessions = info.CurrentlyActive; + result.Data.MaxSessions = info.SeatLimit; + result.Data.CanConnect = (info.CurrentlyActive < info.SeatLimit); + + result.PageSize = 1; + result.Status = ResponseHelper.Success; + } + //} + + ResponseHelper.PopulateV4ResponseData(result); + + return result; + } + + /// + /// Returns all the department audio streams + /// + /// Array of RecipientResult objects for each responding option in the department + [HttpGet("GetDepartmentAudioStreams")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetDepartmentAudioStreams() + { + var result = new DepartmentAudioResult(); + result.Data = new List(); + + var streams = await _voiceService.GetDepartmentAudiosByDepartmentIdAsync(DepartmentId); + foreach (var stream in streams) + { + var data = new DepartmentAudioResultStreamData(); + data.Id = stream.DepartmentAudioId; + data.Name = stream.Name; + data.Url = stream.Data; + + if (string.IsNullOrWhiteSpace(stream.Type)) + { + //if (stream.Data.Contains("broadcastify")) + // data.Type = "hls"; + //else + data.Type = "audio/mp4"; + } + else + { + data.Type = stream.Type; + } + + result.Data.Add(data); + } + + result.PageSize = streams.Count(); + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + + return Ok(result); + } } } diff --git a/Web/Resgrid.Web.ServicesCore/Middleware/ApiTelemetryInitializer.cs b/Web/Resgrid.Web.ServicesCore/Middleware/ApiTelemetryInitializer.cs new file mode 100644 index 00000000..e4a63a01 --- /dev/null +++ b/Web/Resgrid.Web.ServicesCore/Middleware/ApiTelemetryInitializer.cs @@ -0,0 +1,29 @@ +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; +using System; + +namespace Resgrid.Web.Services.Middleware +{ + public class ApiTelemetryInitializer : ITelemetryInitializer + { + public void Initialize(ITelemetry telemetry) + { + var requestTelemetry = telemetry as RequestTelemetry; + // Is this a TrackRequest() ? + if (requestTelemetry == null) return; + int code; + bool parsed = Int32.TryParse(requestTelemetry.ResponseCode, out code); + if (!parsed) return; + if (code >= 400 && code < 500) + { + // If we set the Success property, the SDK won't change it: + requestTelemetry.Success = true; + + // Allow us to filter these requests in the portal: + requestTelemetry.Properties["Overridden400s"] = "true"; + } + // else leave the SDK to set the Success property + } + } +} diff --git a/Web/Resgrid.Web.ServicesCore/Models/v4/Configs/GetConfigResult.cs b/Web/Resgrid.Web.ServicesCore/Models/v4/Configs/GetConfigResult.cs index 0eba9043..513493ce 100644 --- a/Web/Resgrid.Web.ServicesCore/Models/v4/Configs/GetConfigResult.cs +++ b/Web/Resgrid.Web.ServicesCore/Models/v4/Configs/GetConfigResult.cs @@ -70,5 +70,10 @@ public class GetConfigResultData /// How many meters between subsuquent gps locations to allow the position update to go through for units ///
public int UnitLocationMinMeters { get; set; } + + /// + /// API Key for the OpenWeatherAPI + /// + public string OpenWeatherApiKey { get; set; } } } diff --git a/Web/Resgrid.Web.ServicesCore/Models/v4/Voice/CanConnectToVoiceSessionResult.cs b/Web/Resgrid.Web.ServicesCore/Models/v4/Voice/CanConnectToVoiceSessionResult.cs new file mode 100644 index 00000000..423606f0 --- /dev/null +++ b/Web/Resgrid.Web.ServicesCore/Models/v4/Voice/CanConnectToVoiceSessionResult.cs @@ -0,0 +1,34 @@ +namespace Resgrid.Web.Services.Models.v4.Voice +{ + /// + /// Result of checking if the user can connect to a voice session + /// + public class CanConnectToVoiceSessionResult : StandardApiResponseV4Base + { + /// + /// Response Data + /// + public CanConnectToVoiceSessionResultData Data { get; set; } + } + + /// + /// Data needed to verify the user can connect to a voice session + /// + public class CanConnectToVoiceSessionResultData + { + /// + /// Can the User Connect to the voice session + /// + public bool CanConnect { get; set; } + + /// + /// Current active voice session count + /// + public int CurrentSessions { get; set; } + + /// + /// Max session count + /// + public int MaxSessions { get; set; } + } +} diff --git a/Web/Resgrid.Web.ServicesCore/Models/v4/Voice/DepartmentAudioResult.cs b/Web/Resgrid.Web.ServicesCore/Models/v4/Voice/DepartmentAudioResult.cs new file mode 100644 index 00000000..bc161ead --- /dev/null +++ b/Web/Resgrid.Web.ServicesCore/Models/v4/Voice/DepartmentAudioResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Resgrid.Web.Services.Models.v4.Voice +{ + public class DepartmentAudioResult : StandardApiResponseV4Base + { + /// + /// Response Data + /// + public List Data { get; set; } + } + + public class DepartmentAudioResultStreamData + { + public string Id { get; set; } + + public string Name { get; set; } + + public string Url { get; set; } + + public string Type { get; set; } + } +} diff --git a/Web/Resgrid.Web.ServicesCore/Models/v4/Voice/DepartmentVoiceResult.cs b/Web/Resgrid.Web.ServicesCore/Models/v4/Voice/DepartmentVoiceResult.cs index 45370011..91c5b604 100644 --- a/Web/Resgrid.Web.ServicesCore/Models/v4/Voice/DepartmentVoiceResult.cs +++ b/Web/Resgrid.Web.ServicesCore/Models/v4/Voice/DepartmentVoiceResult.cs @@ -22,6 +22,8 @@ public class DepartmentVoiceResultData public string CallerIdName { get; set; } + public string CanConnectApiToken { get; set; } + public List Channels { get; set; } public DepartmentVoiceUserInfoResultData UserInfo { get; set; } @@ -36,6 +38,8 @@ public class DepartmentVoiceChannelResultData public int ConferenceNumber { get; set; } public bool IsDefault { get; set; } + + public string Token { get; set; } } public class DepartmentVoiceUserInfoResultData diff --git a/Web/Resgrid.Web.ServicesCore/Program.cs b/Web/Resgrid.Web.ServicesCore/Program.cs index 1c35e616..977cdb3c 100644 --- a/Web/Resgrid.Web.ServicesCore/Program.cs +++ b/Web/Resgrid.Web.ServicesCore/Program.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading; using Autofac.Extensions.DependencyInjection; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; @@ -10,6 +11,7 @@ public class Program { public static void Main(string[] args) { + ThreadPool.SetMinThreads(200, 200); CreateHostBuilder(args).Build().Run(); } diff --git a/Web/Resgrid.Web.ServicesCore/Resgrid.Web.Services.xml b/Web/Resgrid.Web.ServicesCore/Resgrid.Web.Services.xml index 6e3bb53d..489586df 100644 --- a/Web/Resgrid.Web.ServicesCore/Resgrid.Web.Services.xml +++ b/Web/Resgrid.Web.ServicesCore/Resgrid.Web.Services.xml @@ -3910,6 +3910,18 @@
Voice connection result containing the data needed to connect to a voip session + + + Determines if a user can connect to an voice session, limited to only LiveKit. + + Voice connection result containing the data needed to determine if a user can connect to a voice session + + + + Returns all the department audio streams + + Array of RecipientResult objects for each responding option in the department + Gets or sets the on authentication failed. @@ -5294,6 +5306,11 @@ How many meters between subsuquent gps locations to allow the position update to go through for units + + + API Key for the OpenWeatherAPI + + Custom defined Status for Personnel and Units @@ -8231,6 +8248,36 @@ Default constructor + + + Result of checking if the user can connect to a voice session + + + + + Response Data + + + + + Data needed to verify the user can connect to a voice session + + + + + Can the User Connect to the voice session + + + + + Current active voice session count + + + + + Max session count + + Connects to a voip session @@ -8246,6 +8293,11 @@ Name of the person or unit connecting + + + Response Data + + Response Data diff --git a/Web/Resgrid.Web.ServicesCore/Resgrid.Web.ServicesCore.csproj b/Web/Resgrid.Web.ServicesCore/Resgrid.Web.ServicesCore.csproj index 3d937410..e4c1c296 100644 --- a/Web/Resgrid.Web.ServicesCore/Resgrid.Web.ServicesCore.csproj +++ b/Web/Resgrid.Web.ServicesCore/Resgrid.Web.ServicesCore.csproj @@ -13,6 +13,7 @@ ..\.. ..\..\docker-compose.dcproj Debug;Release;Docker + /subscriptions/bc1ffca7-bf1a-49f9-88f7-b99be887fe9d/resourceGroups/ResgridUSWest/providers/microsoft.insights/components/resgridinsights @@ -47,6 +48,7 @@ + diff --git a/Web/Resgrid.Web.ServicesCore/Startup.cs b/Web/Resgrid.Web.ServicesCore/Startup.cs index 69d02e94..24cd0bf6 100644 --- a/Web/Resgrid.Web.ServicesCore/Startup.cs +++ b/Web/Resgrid.Web.ServicesCore/Startup.cs @@ -56,12 +56,15 @@ using Swashbuckle.AspNetCore.Swagger; using System.Security.Cryptography.X509Certificates; using Resgrid.Web.Services; +using Microsoft.ApplicationInsights.AspNetCore.Extensions; +using Microsoft.ApplicationInsights.Extensibility; namespace Resgrid.Web.ServicesCore { public class Startup { - public IConfigurationRoot Configuration { get; private set; } + //public IConfiguration Configuration { get; } + public IConfigurationRoot Configuration { get; private set; } public ILifetimeScope AutofacContainer { get; private set; } public AutofacServiceLocator Locator { get; private set; } public IServiceCollection Services { get; private set; } @@ -91,7 +94,7 @@ public void ConfigureServices(IServiceCollection services) bool configResult = ConfigProcessor.LoadAndProcessConfig(Configuration["AppOptions:ConfigPath"]); bool envConfigResult = ConfigProcessor.LoadAndProcessEnvVariables(Configuration.AsEnumerable()); - Framework.Logging.Initialize(ExternalErrorConfig.ExternalErrorServiceUrl); + Framework.Logging.Initialize(ExternalErrorConfig.ExternalErrorServiceUrlForApi); //var manager = new ApplicationPartManager(); //manager.ApplicationParts.Add(new AssemblyPart(typeof(Startup).Assembly)); @@ -537,6 +540,17 @@ public void ConfigureServices(IServiceCollection services) services.AddHostedService(); this.Services = services; + + if (Config.ExternalErrorConfig.ApplicationInsightsEnabled) + { + services.AddSingleton(); + + var aiOptions = new ApplicationInsightsServiceOptions(); + aiOptions.InstrumentationKey = ExternalErrorConfig.ApplicationInsightsInstrumentationKey; + aiOptions.ConnectionString = ExternalErrorConfig.ApplicationInsightsConnectionString; + + services.AddApplicationInsightsTelemetry(aiOptions); + } } public void ConfigureContainer(ContainerBuilder builder) diff --git a/Web/Resgrid.WebCore/Areas/User/Apps/src/app/componetns/omnibar/omnibar.component.css b/Web/Resgrid.WebCore/Areas/User/Apps/src/app/componetns/omnibar/omnibar.component.css new file mode 100644 index 00000000..e69de29b diff --git a/Web/Resgrid.WebCore/Areas/User/Apps/src/app/componetns/omnibar/omnibar.component.html b/Web/Resgrid.WebCore/Areas/User/Apps/src/app/componetns/omnibar/omnibar.component.html new file mode 100644 index 00000000..e69de29b diff --git a/Web/Resgrid.WebCore/Areas/User/Apps/src/app/componetns/omnibar/omnibar.component.ts b/Web/Resgrid.WebCore/Areas/User/Apps/src/app/componetns/omnibar/omnibar.component.ts new file mode 100644 index 00000000..e69de29b diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/AccountController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/AccountController.cs index f5a34912..351cd661 100644 --- a/Web/Resgrid.WebCore/Areas/User/Controllers/AccountController.cs +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/AccountController.cs @@ -1,10 +1,13 @@ using System; +using System.Threading; +using System.Threading.Tasks; using System.Web.Mvc; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Resgrid.Model; using Resgrid.Model.Services; using Resgrid.Web.Areas.User.Models.Account; +using Resgrid.Web.Helpers; using IAuthorizationService = Resgrid.Model.Services.IAuthorizationService; namespace Resgrid.Web.Areas.User.Controllers @@ -41,11 +44,37 @@ public AccountController(IDepartmentsService departmentsService, IUsersService u [HttpGet] [Authorize(Roles = SystemRoles.Users)] - public IActionResult DeleteAccount() + public async Task DeleteAccount() { DeleteAccountModel model = new DeleteAccountModel(); + var allDepartments = await _departmentsService.GetAllDepartmentsForUserAsync(UserId); + + foreach (var dm in allDepartments) + { + var department = await _departmentsService.GetDepartmentByIdAsync(dm.DepartmentId); + + if (department != null && department.ManagingUserId == UserId) + model.IsDepartmentOwner = true; + } + + return View(model); + } + + [HttpPost] + [Authorize(Roles = SystemRoles.Users)] + public async Task DeleteAccount(DeleteAccountModel model, CancellationToken cancellationToken) + { + if (model.AreYouSure == false) + ModelState.AddModelError("AreYouSure", "You need to confirm the delete."); + + if (ModelState.IsValid) + { + await _deleteService.DeleteUserAccountAsync(DepartmentId, UserId, UserId, IpAddressHelper.GetRequestIP(Request, true), $"{Request.Headers["User-Agent"]} {Request.Headers["Accept-Language"]}", cancellationToken); + return RedirectToAction("LogOff", "Account", new { area = "" }); + } + return View(model); } } -} \ No newline at end of file +} diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/DepartmentController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/DepartmentController.cs index d41dc801..593aa356 100644 --- a/Web/Resgrid.WebCore/Areas/User/Controllers/DepartmentController.cs +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/DepartmentController.cs @@ -29,6 +29,7 @@ using Elasticsearch.Net; using Microsoft.AspNetCore.Identity; using AuditEvent = Resgrid.Model.Events.AuditEvent; +using Microsoft.AspNetCore.Http; namespace Resgrid.Web.Areas.User.Controllers { @@ -36,6 +37,7 @@ namespace Resgrid.Web.Areas.User.Controllers public class DepartmentController : SecureBaseController { #region Private Members and Constructors + private readonly IDepartmentsService _departmentsService; private readonly IUsersService _usersService; private readonly IActionLogsService _actionLogsService; @@ -59,13 +61,16 @@ public class DepartmentController : SecureBaseController private readonly ICustomStateService _customStateService; private readonly ICqrsProvider _cqrsProvider; private readonly IPrinterProvider _printerProvider; + private readonly IQueueService _queueService; + private readonly IPaymentProviderService _paymentProviderService; public DepartmentController(IDepartmentsService departmentsService, IUsersService usersService, IActionLogsService actionLogsService, IEmailService emailService, IDepartmentGroupsService departmentGroupsService, IUserProfileService userProfileService, IDeleteService deleteService, IInvitesService invitesService, Model.Services.IAuthorizationService authorizationService, IAddressService addressService, ISubscriptionsService subscriptionsService, ILimitsService limitsService, ICallsService callsService, IDepartmentSettingsService departmentSettingsService, IUnitsService unitsService, ICertificationService certificationService, INumbersService numbersService, IScheduledTasksService scheduledTasksService, IPersonnelRolesService personnelRolesService, - IEventAggregator eventAggregator, ICustomStateService customStateService, ICqrsProvider cqrsProvider, IPrinterProvider printerProvider) + IEventAggregator eventAggregator, ICustomStateService customStateService, ICqrsProvider cqrsProvider, IPrinterProvider printerProvider, IQueueService queueService, + IPaymentProviderService paymentProviderService) { _departmentsService = departmentsService; _usersService = usersService; @@ -90,10 +95,14 @@ public DepartmentController(IDepartmentsService departmentsService, IUsersServic _customStateService = customStateService; _cqrsProvider = cqrsProvider; _printerProvider = printerProvider; + _queueService = queueService; + _paymentProviderService = paymentProviderService; } + #endregion Private Members and Constructors #region Invites + [HttpGet] [Authorize(Policy = ResgridResources.Department_Update)] public async Task Invites() @@ -145,7 +154,10 @@ public async Task Invites(InvitesView model, CancellationToken ca var user = _usersService.GetUserByEmail(email); if (user != null) { - ModelState.AddModelError("EmailAddresses", string.Format("The email address {0} is already in use in this department or another. Email address can only be used once per account in the system. If the user previously has a Resgrid account they need to be added via the Add a Person page.", email)); + ModelState.AddModelError("EmailAddresses", + string.Format( + "The email address {0} is already in use in this department or another. Email address can only be used once per account in the system. If the user previously has a Resgrid account they need to be added via the Add a Person page.", + email)); } } @@ -184,16 +196,23 @@ public async Task DeleteInvite(int? inviteId, CancellationToken c return RedirectToAction("Invites", "Department", new { Area = "User" }); } + #endregion Invites #region Settings + [HttpGet] [Authorize(Policy = ResgridResources.Department_Update)] public async Task Settings() { DepartmentSettingsModel model = new DepartmentSettingsModel(); model.Department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); - model.SetUsers(await _departmentsService.GetAllUsersForDepartmentAsync(DepartmentId, false, true), await _departmentsService.GetAllPersonnelNamesForDepartmentAsync(DepartmentId)); + model.SetUsers(await _departmentsService.GetAllUsersForDepartmentAsync(DepartmentId, false, true), + await _departmentsService.GetAllPersonnelNamesForDepartmentAsync(DepartmentId)); + + // Default to PST so there is at least something. Sorry everyone else. + if (String.IsNullOrWhiteSpace(model.Department.TimeZone)) + model.Department.TimeZone = "Pacific Standard Time"; ViewBag.Countries = new SelectList(Countries.CountryNames); ViewBag.TimeZones = new SelectList(TimeZones.Zones, "Key", "Value"); @@ -233,13 +252,22 @@ public async Task Settings() var staffingLevels = await _customStateService.GetActiveStaffingLevelsForDepartmentAsync(DepartmentId); if (staffingLevels == null) { + model.Staffings = _customStateService.GetDefaultPersonStaffings(); model.StaffingLevels = model.UserStateTypes.ToSelectListInt(); } else { + model.Staffings = staffingLevels.GetActiveDetails(); model.StaffingLevels = new SelectList(staffingLevels.GetActiveDetails(), "CustomStateDetailId", "ButtonText"); } + model.SuppressStaffingInfo = await _departmentSettingsService.GetDepartmentStaffingSuppressInfoAsync(DepartmentId, false); + + if (model.SuppressStaffingInfo != null) + { + model.EnableStaffingSupress = model.SuppressStaffingInfo.EnableSupressStaffing; + } + var actionLogs = await _customStateService.GetActivePersonnelStateForDepartmentAsync(DepartmentId); if (actionLogs == null) { @@ -334,7 +362,7 @@ public async Task Settings() [HttpPost] [ValidateAntiForgeryToken] [Authorize(Policy = ResgridResources.Department_Update)] - public async Task Settings(DepartmentSettingsModel model, CancellationToken cancellationToken) + public async Task Settings(DepartmentSettingsModel model, IFormCollection form, CancellationToken cancellationToken) { var auditEvent = new AuditEvent(); auditEvent.DepartmentId = DepartmentId; @@ -353,6 +381,10 @@ public async Task Settings(DepartmentSettingsModel model, Cancell d.ManagingUserId = model.Department.ManagingUserId; d.Use24HourTime = model.Use24HourTime; + ViewBag.Countries = new SelectList(Countries.CountryNames); + ViewBag.TimeZones = new SelectList(TimeZones.Zones, "Key", "Value"); + ViewBag.Users = new SelectList(model.Users, "Key", "Value"); + PersonnelSortOrders personnelSortOrders = PersonnelSortOrders.Default; model.PersonnelSortTypes = personnelSortOrders.ToSelectListInt(); @@ -365,10 +397,12 @@ public async Task Settings(DepartmentSettingsModel model, Cancell var staffingLevels = await _customStateService.GetActiveStaffingLevelsForDepartmentAsync(DepartmentId); if (staffingLevels == null) { + model.Staffings = _customStateService.GetDefaultPersonStaffings(); model.StaffingLevels = model.UserStateTypes.ToSelectListInt(); } else { + model.Staffings = staffingLevels.GetActiveDetails(); model.StaffingLevels = new SelectList(staffingLevels.GetActiveDetails(), "CustomStateDetailId", "ButtonText"); } @@ -396,7 +430,8 @@ public async Task Settings(DepartmentSettingsModel model, Cancell departmentAddress = model.Department.Address; model.User = _usersService.GetUserById(UserId); - model.SetUsers(await _departmentsService.GetAllUsersForDepartmentAsync(DepartmentId, false, true), await _departmentsService.GetAllPersonnelNamesForDepartmentAsync(DepartmentId)); + model.SetUsers(await _departmentsService.GetAllUsersForDepartmentAsync(DepartmentId, false, true), + await _departmentsService.GetAllPersonnelNamesForDepartmentAsync(DepartmentId)); if (!String.IsNullOrWhiteSpace(model.MapZoomLevel)) { @@ -444,6 +479,16 @@ public async Task Settings(DepartmentSettingsModel model, Cancell ModelState.AddModelError("TimeToResetStatus", "If you want to reset status levels you need to supply a time to reset them."); } + if (!String.IsNullOrWhiteSpace(model.MapCenterGpsCoordinatesLatitude) && !LocationHelpers.IsValidLatitude(model.MapCenterGpsCoordinatesLatitude)) + { + ModelState.AddModelError("MapCenterGpsCoordinatesLatitude", "Map Center Latitude value seems invalid, MUST be decimal format."); + } + + if (!String.IsNullOrWhiteSpace(model.MapCenterGpsCoordinatesLongitude) && !LocationHelpers.IsValidLongitude(model.MapCenterGpsCoordinatesLongitude)) + { + ModelState.AddModelError("MapCenterGpsCoordinatesLongitude", "Map Center Longitude value seems invalid, MUST be decimal format."); + } + if (ModelState.IsValid) { departmentAddress = await _addressService.SaveAddressAsync(departmentAddress, cancellationToken); @@ -463,15 +508,30 @@ public async Task Settings(DepartmentSettingsModel model, Cancell if (!String.IsNullOrWhiteSpace(model.RefreshTime)) await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.RefreshTime, DepartmentSettingTypes.BigBoardPageRefresh, cancellationToken); - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.MapHideUnavailable.ToString(), DepartmentSettingTypes.BigBoardHideUnavailable, cancellationToken); - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.DisableAutoAvailable.ToString(), DepartmentSettingTypes.DisabledAutoAvailable, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.MapHideUnavailable.ToString(), DepartmentSettingTypes.BigBoardHideUnavailable, + cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.DisableAutoAvailable.ToString(), DepartmentSettingTypes.DisabledAutoAvailable, + cancellationToken); - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.PersonnelSort.ToString(), DepartmentSettingTypes.PersonnelSortOrder, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.PersonnelSort.ToString(), DepartmentSettingTypes.PersonnelSortOrder, + cancellationToken); await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.UnitsSort.ToString(), DepartmentSettingTypes.UnitsSortOrder, cancellationToken); await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.CallsSort.ToString(), DepartmentSettingTypes.CallsSortOrder, cancellationToken); if (!String.IsNullOrWhiteSpace(model.MapCenterGpsCoordinatesLatitude) && !String.IsNullOrWhiteSpace(model.MapCenterGpsCoordinatesLongitude)) - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.MapCenterGpsCoordinatesLatitude + "," + model.MapCenterGpsCoordinatesLongitude, DepartmentSettingTypes.BigBoardMapCenterGpsCoordinates, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.MapCenterGpsCoordinatesLatitude + "," + model.MapCenterGpsCoordinatesLongitude, + DepartmentSettingTypes.BigBoardMapCenterGpsCoordinates, cancellationToken); + + model.SuppressStaffingInfo = new DepartmentSuppressStaffingInfo(); + + if (form.ContainsKey("staffingLevelsToSupress") && form["staffingLevelsToSupress"].Any()) + { + model.SuppressStaffingInfo.StaffingLevelsToSupress.AddRange(form["staffingLevelsToSupress"].ToString().Split(char.Parse(",")).Select(x => int.Parse(x)).ToList()); + } + + model.SuppressStaffingInfo.EnableSupressStaffing = model.EnableStaffingSupress; + + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, ObjectSerialization.Serialize(model.SuppressStaffingInfo), DepartmentSettingTypes.StaffingSuppressStaffingLevels, cancellationToken); if (!String.IsNullOrWhiteSpace(model.MapCenterPointAddressAddress1)) { @@ -487,7 +547,8 @@ public async Task Settings(DepartmentSettingsModel model, Cancell newAddress.Country = model.MapCenterPointAddressCountry; newAddress = await _addressService.SaveAddressAsync(newAddress, cancellationToken); - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, newAddress.AddressId.ToString(), DepartmentSettingTypes.BigBoardMapCenterAddress, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, newAddress.AddressId.ToString(), DepartmentSettingTypes.BigBoardMapCenterAddress, + cancellationToken); } else { @@ -611,7 +672,8 @@ public async Task ProvisionApiKeyAsync(CancellationToken cancella [Authorize(Policy = ResgridResources.Department_Update)] public async Task ProvisionActiveCallRssKey(CancellationToken cancellationToken) { - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, Guid.NewGuid().ToString("N"), DepartmentSettingTypes.RssFeedKeyForActiveCalls, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, Guid.NewGuid().ToString("N"), DepartmentSettingTypes.RssFeedKeyForActiveCalls, + cancellationToken); return RedirectToAction("Api", "Department", new { Area = "User" }); } @@ -678,14 +740,16 @@ public async Task Address(SettingsAddressModel model, Cancellatio return View(model); } + #endregion Settings #region User States + [Authorize(Policy = ResgridResources.Department_Update)] public async Task SetUserResponding(string userId) { await _actionLogsService.SetUserActionAsync(userId, (await _departmentsService.GetDepartmentByUserIdAsync(UserId)).DepartmentId, - (int)ActionTypes.Responding); + (int)ActionTypes.Responding); return RedirectToAction("Index", "Personnel", new { area = "User" }); } @@ -694,7 +758,7 @@ await _actionLogsService.SetUserActionAsync(userId, (await _departmentsService.G public async Task SetUserNotResponding(string userId) { await _actionLogsService.SetUserActionAsync(userId, (await _departmentsService.GetDepartmentByUserIdAsync(UserId)).DepartmentId, - (int)ActionTypes.NotResponding); + (int)ActionTypes.NotResponding); return RedirectToAction("Index", "Personnel", new { area = "User" }); } @@ -703,7 +767,7 @@ await _actionLogsService.SetUserActionAsync(userId, (await _departmentsService.G public async Task SetUserStandingBy(string userId) { await _actionLogsService.SetUserActionAsync(userId, (await _departmentsService.GetDepartmentByUserIdAsync(UserId)).DepartmentId, - (int)ActionTypes.StandingBy); + (int)ActionTypes.StandingBy); return RedirectToAction("Index", "Personnel", new { area = "User" }); } @@ -712,13 +776,15 @@ await _actionLogsService.SetUserActionAsync(userId, (await _departmentsService.G public async Task SetUserOnScene(string userId) { await _actionLogsService.SetUserActionAsync(userId, (await _departmentsService.GetDepartmentByUserIdAsync(UserId)).DepartmentId, - (int)ActionTypes.OnScene); + (int)ActionTypes.OnScene); return RedirectToAction("Index", "Personnel", new { area = "User" }); } + #endregion User States #region Call Settings + [HttpGet] [Authorize(Policy = ResgridResources.Department_Update)] public async Task CallSettings() @@ -1011,9 +1077,11 @@ public async Task DeleteCallEmailSettings(CancellationToken cance return RedirectToAction("CallSettings"); } + #endregion Call Settings #region Unit Settings + [HttpGet] [Authorize(Policy = ResgridResources.Department_Update)] public async Task UnitSettings() @@ -1047,9 +1115,11 @@ public async Task NewUnitType(UnitSettingsView model, Cancellatio return RedirectToAction("Types"); } + #endregion Unit Settings #region Types + [HttpGet] [Authorize(Policy = ResgridResources.Department_Update)] public async Task Types() @@ -1138,9 +1208,11 @@ public async Task NewCallType(DepartmentTypesView model, Cancella return RedirectToAction("Types"); } + #endregion Types #region Text Messaging + [HttpGet] [Authorize(Policy = ResgridResources.Department_Update)] public async Task TextSettings() @@ -1174,18 +1246,23 @@ public async Task TextSettings(TextSetupModel model, Cancellation model.TextCallType = textImportFormat.Value; if (model.EnableTextCommand && model.EnableTextToCall && String.IsNullOrWhiteSpace(model.DepartmentTextToCallSourceNumbers)) - ModelState.AddModelError("DepartmentTextToCallSourceNumbers", "You have enabled Text-To-Call and Text Command, to prevent all personel command texts being imported as calls you must supply the phone numbers that text dispatches will orginiate from."); + ModelState.AddModelError("DepartmentTextToCallSourceNumbers", + "You have enabled Text-To-Call and Text Command, to prevent all personel command texts being imported as calls you must supply the phone numbers that text dispatches will orginiate from."); if (ModelState.IsValid) { if (!String.IsNullOrWhiteSpace(model.DepartmentTextToCallSourceNumbers)) - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.DepartmentTextToCallSourceNumbers, DepartmentSettingTypes.TextToCallSourceNumbers, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.DepartmentTextToCallSourceNumbers, DepartmentSettingTypes.TextToCallSourceNumbers, + cancellationToken); else await _departmentSettingsService.DeleteSettingAsync(DepartmentId, DepartmentSettingTypes.TextToCallSourceNumbers, cancellationToken); - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.TextCallType.ToString(), DepartmentSettingTypes.TextToCallImportFormat, cancellationToken); - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.EnableTextToCall.ToString(), DepartmentSettingTypes.EnableTextToCall, cancellationToken); - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.EnableTextCommand.ToString(), DepartmentSettingTypes.EnableTextCommand, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.TextCallType.ToString(), DepartmentSettingTypes.TextToCallImportFormat, + cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.EnableTextToCall.ToString(), DepartmentSettingTypes.EnableTextToCall, + cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.EnableTextCommand.ToString(), DepartmentSettingTypes.EnableTextCommand, + cancellationToken); } return View(model); @@ -1265,9 +1342,11 @@ public async Task GetCallTextTypes() return Json(callEmailTypes); } + #endregion Text Messaging #region Stations Grid + [HttpGet] [Authorize(Policy = ResgridResources.Department_View)] public async Task GetStationsForGrid() @@ -1303,9 +1382,11 @@ public async Task GetDepartmentTypes() return Json(new SelectList(model.DepartmentTypes)); } + #endregion Stations Grid #region Recipients + [HttpGet] [Authorize(Policy = ResgridResources.Department_View)] public async Task GetRecipientsForGrid(int filter = 0, bool filterSelf = false, bool filterNotInGroup = false, int ignoreGroupId = 0) @@ -1356,8 +1437,8 @@ public async Task GetRecipientsForGrid(int filter = 0, bool filte respondingTo.Name = p.Value.FullName.AsFirstNameLastName; if ((filterSelf && p.Value.UserId != UserId) || - (filterNotInGroup && !groupUsers.Any(x => x.UserId == p.Value.UserId)) || - (!filterSelf && !filterNotInGroup)) + (filterNotInGroup && !groupUsers.Any(x => x.UserId == p.Value.UserId)) || + (!filterSelf && !filterNotInGroup)) result.Add(respondingTo); //else // result.Add(respondingTo); @@ -1374,9 +1455,11 @@ public PartialViewResult RecipientsGrid() { return PartialView("_RecipientsGridPartial"); } + #endregion Recipients #region Aync Site Parts + [HttpGet] [Authorize(Policy = ResgridResources.Department_View)] public async Task GetSubscriptionLimitWarning() @@ -1385,7 +1468,10 @@ public async Task GetSubscriptionLimitWarning() return Json(new { title = "System Notice", message = Config.NoticeConfig.DashboardToastNotice }); if (!await _limitsService.ValidateDepartmentIsWithinLimitsAsync(DepartmentId)) - return Json(new { title = "Department Limits", message = "The department is at or has exceeded some limits for the current subscription plan. Some functionality may be impacted." }); + return Json(new + { + title = "Department Limits", message = "The department is at or has exceeded some limits for the current subscription plan. Some functionality may be impacted." + }); return Json(new { title = "", message = "" }); } @@ -1414,9 +1500,11 @@ public async Task UpgradeButton() return Content(""); } + #endregion Aync Site Parts #region Async Validation Methods + //[HttpPost] //[Authorize(Policy = ResgridResources.Department_View)] //public async Task TestEmailSettings(EmailTestInput input) @@ -1456,9 +1544,11 @@ public async Task ValidateAddress(Address address) return Content(returnText); } + #endregion Async Validation Methods #region Setup Wizard + [HttpGet] [Authorize(Policy = ResgridResources.Department_View)] public async Task SetupWizard() @@ -1485,7 +1575,8 @@ public async Task SubmitSetupWizard([FromBody] SetupWizardFormPay var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); department.TimeZone = formCollection["Department.TimeZone"]; - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, formCollection["DisableAutoAvailable"], DepartmentSettingTypes.DisabledAutoAvailable, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, formCollection["DisableAutoAvailable"], DepartmentSettingTypes.DisabledAutoAvailable, + cancellationToken); Address address = null; if (department.AddressId.HasValue) @@ -1557,9 +1648,11 @@ public async Task SubmitSetupWizard([FromBody] SetupWizardFormPay return new JsonResult("{}"); } + #endregion Setup Wizard #region Dispatch Settings + [HttpGet] [Authorize(Policy = ResgridResources.Department_Update)] public async Task ShiftSettings() @@ -1577,7 +1670,8 @@ public async Task ShiftSettings(ShiftSettingsView model, Cancella { if (ModelState.IsValid) { - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.AllowSignupsForMultipleShiftGroups.ToString(), DepartmentSettingTypes.AllowSignupsForMultipleShiftGroups, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.AllowSignupsForMultipleShiftGroups.ToString(), + DepartmentSettingTypes.AllowSignupsForMultipleShiftGroups, cancellationToken); model.SaveSuccess = true; return View(model); @@ -1586,9 +1680,11 @@ public async Task ShiftSettings(ShiftSettingsView model, Cancella model.SaveSuccess = false; return View(model); } + #endregion Dispatch Settings #region Dispatch Settings + [HttpGet] [Authorize(Policy = ResgridResources.Department_Update)] public async Task DispatchSettings() @@ -1642,10 +1738,14 @@ public async Task DispatchSettings(DispatchSettingsView model, Ca if (ModelState.IsValid) { - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.DispatchShiftInsteadOfGroup.ToString(), DepartmentSettingTypes.DispatchShiftInsteadOfGroup, cancellationToken); - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.AutoSetStatusForShiftPersonnel.ToString(), DepartmentSettingTypes.AutoSetStatusForShiftDispatchPersonnel, cancellationToken); - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.ShiftDispatchStatus.ToString(), DepartmentSettingTypes.ShiftCallDispatchPersonnelStatusToSet, cancellationToken); - await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.ShiftClearStatus.ToString(), DepartmentSettingTypes.ShiftCallReleasePersonnelStatusToSet, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.DispatchShiftInsteadOfGroup.ToString(), + DepartmentSettingTypes.DispatchShiftInsteadOfGroup, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.AutoSetStatusForShiftPersonnel.ToString(), + DepartmentSettingTypes.AutoSetStatusForShiftDispatchPersonnel, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.ShiftDispatchStatus.ToString(), + DepartmentSettingTypes.ShiftCallDispatchPersonnelStatusToSet, cancellationToken); + await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, model.ShiftClearStatus.ToString(), + DepartmentSettingTypes.ShiftCallReleasePersonnelStatusToSet, cancellationToken); model.SaveSuccess = true; return View(model); @@ -1654,8 +1754,90 @@ public async Task DispatchSettings(DispatchSettingsView model, Ca model.SaveSuccess = false; return View(model); } + #endregion Dispatch Settings + #region Delete Department + + [HttpGet] + [Authorize(Policy = ResgridResources.Department_Update)] + public async Task DeleteDepartment() + { + DeleteDepartmentView model = new DeleteDepartmentView(); + model.CurrentDeleteRequest = await _queueService.GetPendingDeleteDepartmentQueueItemAsync(DepartmentId); + model.Department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); + + if (model.CurrentDeleteRequest != null) + { + model.Profile = await _userProfileService.GetProfileByUserIdAsync(model.CurrentDeleteRequest.QueuedByUserId); + } + + return View(model); + } + + [HttpPost] + [Authorize(Policy = ResgridResources.Department_Update)] + public async Task DeleteDepartment(DeleteDepartmentView model, CancellationToken cancellationToken) + { + if (model.AreYouSure == false) + ModelState.AddModelError("AreYouSure", "You need to confirm the delete."); + + if (ModelState.IsValid) + { + var result = await _deleteService.DeleteDepartment(DepartmentId, UserId, IpAddressHelper.GetRequestIP(Request, true), + $"{Request.Headers["User-Agent"]} {Request.Headers["Accept-Language"]}", cancellationToken); + + var stripeCustomer = await _departmentSettingsService.GetStripeCustomerIdForDepartmentAsync(DepartmentId); + var currentSub = await _paymentProviderService.GetActiveStripeSubscriptionAsync(stripeCustomer, cancellationToken); + var pttSub = await _paymentProviderService.GetActivePTTStripeSubscriptionAsync(stripeCustomer, cancellationToken); + + if (pttSub != null) + { + PlanAddon addon = new PlanAddon(); + await _paymentProviderService.ModifyPTTAddonSubscription(stripeCustomer, 0, addon, cancellationToken); + } + + if (currentSub != null) + { + await _paymentProviderService.CancelSubscriptionAsync(stripeCustomer, cancellationToken); + } + + return RedirectToAction("Settings", "Department", new { area = "User" }); + } + + return View(model); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Department_Update)] + public async Task CancelDepartmentDeleteRequest(CancellationToken cancellationToken) + { + var queueItem = await _queueService.GetPendingDeleteDepartmentQueueItemAsync(DepartmentId); + + if (queueItem != null) + { + var auditEvent = new AuditEvent(); + auditEvent.Before = null; + auditEvent.DepartmentId = DepartmentId; + auditEvent.UserId = UserId; + auditEvent.Type = AuditLogTypes.DeleteDepartmentRequestedCancelled; + auditEvent.After = null; + auditEvent.Successful = true; + auditEvent.IpAddress = IpAddressHelper.GetRequestIP(Request, true); + auditEvent.ServerName = Environment.MachineName; + auditEvent.UserAgent = $"{Request.Headers["User-Agent"]} {Request.Headers["Accept-Language"]}"; + _eventAggregator.SendMessage(auditEvent); + + var profile = await _userProfileService.GetProfileByUserIdAsync(UserId); + + await _queueService.CancelPendingDepartmentDeletionRequest(DepartmentId, profile.FullName.AsFirstNameLastName, cancellationToken); + } + + return RedirectToAction("Settings", "Department", new { area = "User" }); + } + + #endregion Delete Department + [HttpGet] [Authorize(Policy = ResgridResources.Department_Update)] public async Task GetPrinterNetPrinters(string key) diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/DispatchController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/DispatchController.cs index 5c74dae9..9bb21511 100644 --- a/Web/Resgrid.WebCore/Areas/User/Controllers/DispatchController.cs +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/DispatchController.cs @@ -225,6 +225,7 @@ public async Task NewCall(NewCallView model, IFormCollection coll List dispatchingRoleIds = new List(); List activeProtocols = new List(); List pendingProtocols = new List(); + List linkedCalls = new List(); foreach (var key in collection.Keys) { @@ -260,6 +261,11 @@ public async Task NewCall(NewCallView model, IFormCollection coll if (collection[key] == "1") pendingProtocols.Add(pendingProtocolId); } + else if (key.ToString().StartsWith("linkedCall_")) + { + var linkedCallId = int.Parse(key.ToString().Replace("linkedCall_", "")); + linkedCalls.Add(linkedCallId); + } } model.Call.Dispatches = new Collection(); @@ -347,6 +353,26 @@ public async Task NewCall(NewCallView model, IFormCollection coll } } + model.Call.References = new List(); + if (linkedCalls.Any()) + { + foreach (var id in linkedCalls) + { + if (!model.Call.References.Any(x => x.TargetCallId == id)) + { + CallReference reference = new CallReference(); + reference.TargetCallId = id; + reference.AddedOn = DateTime.UtcNow; + reference.AddedByUserId = UserId; + + if (collection.ContainsKey($"linkedCallNote_{id}")) + reference.Note = collection[$"linkedCallNote_{id}"]; + + model.Call.References.Add(reference); + } + } + } + if (model.Call.UnitDispatches != null && model.Call.UnitDispatches.Any()) { foreach (var unitDispatch in model.Call.UnitDispatches) @@ -455,6 +481,7 @@ public async Task UpdateCall(int callId) UpdateCallView model = new UpdateCallView(); model = await FillUpdateCallView(model); model.Call = await _callsService.GetCallByIdAsync(callId); + model.Call = await _callsService.PopulateCallData(model.Call, true, true, true, true, true, true, true, true); model.CallPriority = model.Call.Priority; if (!String.IsNullOrEmpty(model.Call.GeoLocationData)) @@ -480,7 +507,7 @@ public async Task UpdateCall(UpdateCallView model, IFormCollectio if (ModelState.IsValid) { var call = await _callsService.GetCallByIdAsync(model.Call.CallId); - call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true); + call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true, true); call.NatureOfCall = System.Net.WebUtility.HtmlDecode(model.Call.NatureOfCall); call.Notes = System.Net.WebUtility.HtmlDecode(model.Call.Notes); @@ -513,6 +540,8 @@ public async Task UpdateCall(UpdateCallView model, IFormCollectio List dispatchingGroupIds = new List(); List dispatchingUnitIds = new List(); List dispatchingRoleIds = new List(); + List linkedCalls = new List(); + foreach (var key in collection.Keys) { if (key.ToString().StartsWith("dispatchUser_")) @@ -535,6 +564,11 @@ public async Task UpdateCall(UpdateCallView model, IFormCollectio var roleId = int.Parse(key.ToString().Replace("dispatchRole_", "")); dispatchingRoleIds.Add(roleId); } + else if (key.ToString().StartsWith("linkedCall_")) + { + var linkedCallId = int.Parse(key.ToString().Replace("linkedCall_", "")); + linkedCalls.Add(linkedCallId); + } } //await _callsService.DeleteDispatchesAsync(call.Dispatches.ToList(), cancellationToken); @@ -644,6 +678,38 @@ public async Task UpdateCall(UpdateCallView model, IFormCollectio } } + if (linkedCalls.Any()) + { + var callLinksToRemove = call.References.Select(x => x.TargetCallId).Where(y => !linkedCalls.Contains(y)).ToList(); + + foreach (var id in callLinksToRemove) + { + var callRef = call.References.First(x => x.TargetCallId == id); + call.References.Remove(callRef); + + await _callsService.DeleteCallReferenceAsync(callRef, cancellationToken); + } + + foreach (var id in linkedCalls) + { + if (id == call.CallId) + continue; // Can't link to current call. + + if (!call.References.Any(x => x.TargetCallId == id)) + { + CallReference reference = new CallReference(); + reference.TargetCallId = id; + reference.AddedOn = DateTime.UtcNow; + reference.AddedByUserId = UserId; + + if (collection.ContainsKey($"linkedCallNote_{id}")) + reference.Note = collection[$"linkedCallNote_{id}"]; + + call.References.Add(reference); + } + } + } + await _callsService.SaveCallAsync(call, cancellationToken); if (model.RebroadcastCall) @@ -721,8 +787,8 @@ public async Task ViewCall(int callId) model.CallPriority = (CallPriority)model.Call.Priority; model.Stations = await _departmentGroupsService.GetAllStationGroupsForDepartmentAsync(DepartmentId); model.Protocols = await _protocolsService.GetAllProtocolsForDepartmentAsync(DepartmentId); - - model.Call = await _callsService.PopulateCallData(model.Call, true, true, true, true, true, true, true); + model.ChildCalls = await _callsService.GetChildCallsForCallAsync(callId); + model.Call = await _callsService.PopulateCallData(model.Call, true, true, true, true, true, true, true, true); if (model.Stations == null) model.Stations = new List(); @@ -755,6 +821,8 @@ public async Task AddArchivedCall() model.Call = new Call(); model = await FillNewCallView(model); model.Call.LoggedOn = DateTime.UtcNow.TimeConverter(model.Department); + model.Call.ReportingUserId = UserId; + model.CallStates = model.CallState.ToSelectList(); return View(model); } @@ -791,6 +859,8 @@ public async Task AddArchivedCall(NewCallView model, IFormCollect List dispatchingGroupIds = new List(); List dispatchingUnitIds = new List(); List dispatchingRoleIds = new List(); + List linkedCalls = new List(); + foreach (var key in collection.Keys) { if (key.ToString().StartsWith("dispatchUser_")) @@ -813,6 +883,11 @@ public async Task AddArchivedCall(NewCallView model, IFormCollect var roleId = int.Parse(key.ToString().Replace("dispatchRole_", "")); dispatchingRoleIds.Add(roleId); } + else if (key.ToString().StartsWith("linkedCall_")) + { + var linkedCallId = int.Parse(key.ToString().Replace("linkedCall_", "")); + linkedCalls.Add(linkedCallId); + } } // Add all users dispatch's @@ -871,7 +946,31 @@ public async Task AddArchivedCall(NewCallView model, IFormCollect } } + model.Call.References = new List(); + if (linkedCalls.Any()) + { + foreach (var id in linkedCalls) + { + if (!model.Call.References.Any(x => x.TargetCallId == id)) + { + CallReference reference = new CallReference(); + reference.TargetCallId = id; + reference.AddedOn = DateTime.UtcNow; + reference.AddedByUserId = UserId; + + if (collection.ContainsKey($"linkedCallNote_{id}")) + reference.Note = collection[$"linkedCallNote_{id}"]; + + model.Call.References.Add(reference); + } + } + } + model.Call.CallSource = (int)CallSources.User; + model.Call.ClosedByUserId = UserId; + model.Call.ClosedOn = DateTime.UtcNow; + model.Call.CompletedNotes = System.Net.WebUtility.HtmlDecode(model.ClosedCallNotes); + model.Call.State = (int)model.CallState; if (!string.IsNullOrWhiteSpace(model.Call.GeoLocationData) && model.Call.GeoLocationData.Length > 1 && string.IsNullOrWhiteSpace(model.Call.Address)) { @@ -977,7 +1076,7 @@ public async Task FlagCallNote(int callId, int callNoteId) if (call == null) Unauthorized(); - call = await _callsService.PopulateCallData(call, false, false, true, false, false, false, false); + call = await _callsService.PopulateCallData(call, false, false, true, false, false, false, false, false); var department = await _departmentsService.GetDepartmentByIdAsync(call.DepartmentId); if (call.CallNotes == null || !call.CallNotes.Any()) @@ -1019,7 +1118,7 @@ public async Task FlagCallNote(FlagCallNoteView model, Cancellati if (call == null) Unauthorized(); - call = await _callsService.PopulateCallData(call, false, false, true, false, false, false, false); + call = await _callsService.PopulateCallData(call, false, false, true, false, false, false, false, false); var department = await _departmentsService.GetDepartmentByIdAsync(call.DepartmentId); if (call.CallNotes == null || !call.CallNotes.Any()) @@ -1085,7 +1184,7 @@ public async Task GetCallNotes(int callId) Unauthorized(); call.Department = await _departmentsService.GetDepartmentByIdAsync(call.DepartmentId); - call = await _callsService.PopulateCallData(call, false, false, true, false, false, false, false); + call = await _callsService.PopulateCallData(call, false, false, true, false, false, false, false, false); var personnelNames = await _departmentsService.GetAllPersonnelNamesForDepartmentAsync(DepartmentId); List callNotes = new List(); @@ -1136,8 +1235,9 @@ public async Task CallExport(int callId) model.ActionLogs = (await _actionLogsService.GetActionLogsForCallAsync(model.Call.DepartmentId, callId)).OrderBy(x => x.UserId).OrderBy(y => y.Timestamp).ToList(); model.Groups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(DepartmentId); model.Units = await _unitsService.GetUnitsForDepartmentAsync(DepartmentId); - model.Call = await _callsService.PopulateCallData(model.Call, true, true, true, true, true, true, true); + model.Call = await _callsService.PopulateCallData(model.Call, true, true, true, true, true, true, true, true); model.Names = await _departmentsService.GetAllPersonnelNamesForDepartmentAsync(DepartmentId); + model.ChildCalls = await _callsService.GetChildCallsForCallAsync(callId); return View(model); } @@ -1170,7 +1270,7 @@ public async Task CallExportEx(string query) Unauthorized(); var model = new CallExportView(); - model.Call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true); + model.Call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true, true); model.CallLogs = await _workLogsService.GetCallLogsForCallAsync(call.CallId); model.Department = await _departmentsService.GetDepartmentByIdAsync(model.Call.DepartmentId, false); model.UnitStates = (await _unitsService.GetUnitStatesForCallAsync(model.Call.DepartmentId, call.CallId)).OrderBy(x => x.UnitId).OrderBy(y => y.Timestamp).ToList(); @@ -1178,6 +1278,7 @@ public async Task CallExportEx(string query) model.Groups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(model.Call.DepartmentId); model.Units = await _unitsService.GetUnitsForDepartmentAsync(DepartmentId); model.Names = await _departmentsService.GetAllPersonnelNamesForDepartmentAsync(DepartmentId); + model.ChildCalls = await _callsService.GetChildCallsForCallAsync(call.CallId); return View(model); } @@ -1197,7 +1298,7 @@ public async Task CallExportEx(string query) Unauthorized(); var model = new CallExportView(); - model.Call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true); + model.Call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true, true); model.CallLogs = await _workLogsService.GetCallLogsForCallAsync(call.CallId); model.Department = await _departmentsService.GetDepartmentByIdAsync(model.Call.DepartmentId, false); model.UnitStates = (await _unitsService.GetUnitStatesForCallAsync(model.Call.DepartmentId, call.CallId)).OrderBy(x => x.UnitId).OrderBy(y => y.Timestamp).ToList(); @@ -1298,7 +1399,7 @@ public async Task GetCallDispatchAudio(int callId) Unauthorized(); var call = await _callsService.GetCallByIdAsync(callId); - call = await _callsService.PopulateCallData(call, false, true, false, false, false, false, false); + call = await _callsService.PopulateCallData(call, false, true, false, false, false, false, false, false); if (call.Attachments != null && call.Attachments.Count > 0) { @@ -1437,7 +1538,7 @@ public async Task GetAllDispatchesForCall(int callId) List dispatchJson = new List(); var users = await _departmentsService.GetAllUsersForDepartmentUnlimitedMinusDisabledAsync(DepartmentId); var call = await _callsService.GetCallByIdAsync(callId); - call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true); + call = await _callsService.PopulateCallData(call, true, true, true, true, true, true, true, true); foreach (var userDispatch in call.Dispatches) { @@ -1812,6 +1913,33 @@ public async Task GetCallPriorities() return Json(callPrioritiesJson); } + [HttpGet] + [Authorize(Policy = ResgridResources.Call_View)] + public async Task GetCallsForSelectList(string term) + { + CallSelectListJson callSelectJson = new CallSelectListJson(); + callSelectJson.results = new List(); + + var calls = await _callsService.GetAllCallsByDepartmentDateRangeAsync(DepartmentId, DateTime.UtcNow.AddDays(-14), DateTime.UtcNow); + var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); + calls = calls.OrderByDescending(x => x.LoggedOn).ToList(); + + if (calls != null && calls.Any()) + { + foreach (var call in calls) + { + CallSelectListJsonResult json = new CallSelectListJsonResult(); + json.id = call.CallId; + json.text = $"{call.Number} - {call.GetStateText()} - {call.LoggedOn.FormatForDepartment(department)} - {call.Name}"; + + if (String.IsNullOrWhiteSpace(term) || json.text.Contains(term)) + callSelectJson.results.Add(json); + } + } + + return Json(callSelectJson); + } + #region Private Helpers private async Task FillNewCallView(NewCallView model) { diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/DocumentsController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/DocumentsController.cs index 57432349..3830a742 100644 --- a/Web/Resgrid.WebCore/Areas/User/Controllers/DocumentsController.cs +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/DocumentsController.cs @@ -60,7 +60,7 @@ public async Task NewDocument() [Authorize(Policy = ResgridResources.Documents_View)] public async Task GetDepartmentDocumentCategories() { - return Json(_documentsService.GetDistinctCategoriesByDepartmentIdAsync(DepartmentId)); + return Json(await _documentsService.GetDistinctCategoriesByDepartmentIdAsync(DepartmentId)); } [Authorize(Policy = ResgridResources.Documents_View)] diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/HomeController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/HomeController.cs index f0f5b419..ff415d09 100644 --- a/Web/Resgrid.WebCore/Areas/User/Controllers/HomeController.cs +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/HomeController.cs @@ -28,6 +28,11 @@ using Resgrid.Web.Areas.User.Models.BigBoardX; using Resgrid.Model.Identity; using IdentityUser = Resgrid.Model.Identity.IdentityUser; +using Microsoft.Extensions.Localization; +using System.Reflection; +using Resgrid.Localization; +using Microsoft.AspNetCore.Localization; +using System.Web; namespace Resgrid.Web.Areas.User.Controllers { @@ -37,6 +42,7 @@ namespace Resgrid.Web.Areas.User.Controllers public class HomeController : SecureBaseController { #region Private Members and Constructors + private readonly IStringLocalizer _localizer; private readonly IDepartmentsService _departmentsService; private readonly IUsersService _usersService; private readonly IActionLogsService _actionLogsService; @@ -56,12 +62,14 @@ public class HomeController : SecureBaseController private readonly IEventAggregator _eventAggregator; private readonly IOptions _appOptionsAccessor; private readonly UserManager _userManager; + private readonly ISubscriptionsService _subscriptionsService; public HomeController(IDepartmentsService departmentsService, IUsersService usersService, IActionLogsService actionLogsService, IUserStateService userStateService, IDepartmentGroupsService departmentGroupsService, Resgrid.Model.Services.IAuthorizationService authorizationService, IUserProfileService userProfileService, ICallsService callsService, IGeoLocationProvider geoLocationProvider, IDepartmentSettingsService departmentSettingsService, IUnitsService unitsService, IAddressService addressService, IPersonnelRolesService personnelRolesService, IPushService pushService, ILimitsService limitsService, - ICustomStateService customStateService, IEventAggregator eventAggregator, IOptions appOptionsAccessor, UserManager userManager) + ICustomStateService customStateService, IEventAggregator eventAggregator, IOptions appOptionsAccessor, UserManager userManager, + IStringLocalizerFactory factory, ISubscriptionsService subscriptionsService) { _departmentsService = departmentsService; _usersService = usersService; @@ -82,6 +90,9 @@ public HomeController(IDepartmentsService departmentsService, IUsersService user _eventAggregator = eventAggregator; _appOptionsAccessor = appOptionsAccessor; _userManager = userManager; + _subscriptionsService = subscriptionsService; + + _localizer = factory.Create("Home.Dashboard", new AssemblyName(typeof(SupportedLocales).GetTypeInfo().Assembly.FullName).Name); } #endregion Private Members and Constructors @@ -93,7 +104,7 @@ public async Task Dashboard(bool firstRun = false) var staffingLevel = await _userStateService.GetLastUserStateByUserIdAsync(UserId); model.UserState = staffingLevel.State; - model.StateNote = staffingLevel.Note; + //model.StateNote = staffingLevel.Note; var staffingLevels = await _customStateService.GetActiveStaffingLevelsForDepartmentAsync(DepartmentId); if (staffingLevels == null) @@ -334,6 +345,7 @@ public async Task EditUserProfile(string userId) ViewBag.Carriers = model.Carrier.ToSelectList().OrderBy(x => x.Text); ViewBag.Countries = new SelectList(Countries.CountryNames); ViewBag.TimeZones = new SelectList(TimeZones.Zones, "Key", "Value"); + ViewBag.Languages = new SelectList(SupportedLocales.SupportedLanguagesMap, "Key", "Value"); model.Groups = new SelectList(groups, "DepartmentGroupId", "Name"); var group = await _departmentGroupsService.GetGroupForUserAsync(userId, DepartmentId); @@ -420,6 +432,13 @@ public async Task EditUserProfile(string userId) } model.EnableSms = model.Profile.SendSms; + var payment = await _subscriptionsService.GetCurrentPaymentForDepartmentAsync(DepartmentId); + + if (payment != null) + model.IsFreePlan = payment.IsFreePlan(); + + if (String.IsNullOrWhiteSpace(model.Profile.Language)) + model.Profile.Language = "en"; return View(model); } @@ -447,6 +466,7 @@ public async Task EditUserProfile(EditProfileModel model, IFormCo ViewBag.Carriers = model.Carrier.ToSelectList(); ViewBag.Countries = new SelectList(Countries.CountryNames); ViewBag.TimeZones = new SelectList(TimeZones.Zones, "Key", "Value"); + ViewBag.Languages = new SelectList(SupportedLocales.SupportedLanguagesMap, "Key", "Value"); if (!String.IsNullOrEmpty(model.Profile.MobileNumber)) { @@ -595,6 +615,7 @@ public async Task EditUserProfile(EditProfileModel model, IFormCo savedProfile.HomeNumber = model.Profile.HomeNumber; savedProfile.IdentificationNumber = model.Profile.IdentificationNumber; savedProfile.TimeZone = model.Profile.TimeZone; + savedProfile.Language = model.Profile.Language; if (model.CanEnableVoice) { @@ -688,6 +709,12 @@ public async Task EditUserProfile(EditProfileModel model, IFormCo savedProfile.MailingAddressId = mailingAddress.AddressId; } + if (model.IsFreePlan) + { + savedProfile.SendSms = false; + savedProfile.SendMessageSms = false; + } + savedProfile.LastUpdated = DateTime.UtcNow; await _userProfileService.SaveProfileAsync(DepartmentId, savedProfile, cancellationToken); @@ -735,6 +762,14 @@ public async Task EditUserProfile(EditProfileModel model, IFormCo _departmentsService.InvalidateDepartmentMembers(); _usersService.ClearCacheForDepartment(DepartmentId); + if (!String.IsNullOrWhiteSpace(savedProfile.Language)) + { + Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(savedProfile.Language)), new CookieOptions { Expires = DateTime.UtcNow.AddYears(1) }); + // This guy I think is causing issues with like DateTime rendering mm/dd/yy vs dd/mm/yy, so need to look into that more. -SJ + //Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo(savedProfile.Language); + Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(savedProfile.Language); + } + return RedirectToAction("Index", "Personnel", new { area = "User" }); } @@ -846,18 +881,18 @@ public async Task SetActionForUser(string userId, int actionType) private async Task Unsubscribe(string emailAddress) { - try - { - var client = new RestClient("https://app.mailerlite.com"); - var request = new RestRequest("/api/v1/subscribers/unsubscribe/", Method.Post); - request.AddObject(new - { - apiKey = "QDrnoEf6hBONlGye26aZFh5Iv1KEgdJM", - email = emailAddress - }); - var response = await client.ExecuteAsync(request); - } - catch { } + //try + //{ + // var client = new RestClient("https://app.mailerlite.com"); + // var request = new RestRequest("/api/v1/subscribers/unsubscribe/", Method.Post); + // request.AddObject(new + // { + // apiKey = "QDrnoEf6hBONlGye26aZFh5Iv1KEgdJM", + // email = emailAddress + // }); + // var response = await client.ExecuteAsync(request); + //} + //catch { } } } } diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/LinksController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/LinksController.cs index 845a8056..2efb5774 100644 --- a/Web/Resgrid.WebCore/Areas/User/Controllers/LinksController.cs +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/LinksController.cs @@ -54,7 +54,9 @@ public async Task Index() model.CanCreateLinks = await _limitsService.CanDepartmentUseLinksAsync(DepartmentId); var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); - model.Code = department.LinkCode; + + if (department != null) + model.Code = department.LinkCode; return View(model); } diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/NotesController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/NotesController.cs index 8f8c908e..fedbb7f4 100644 --- a/Web/Resgrid.WebCore/Areas/User/Controllers/NotesController.cs +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/NotesController.cs @@ -150,7 +150,7 @@ public async Task Delete(int noteId, CancellationToken cancellati [HttpGet] public async Task GetDepartmentNotesCategories() { - return Json(_notesService.GetDistinctCategoriesByDepartmentIdAsync(DepartmentId)); + return Json(await _notesService.GetDistinctCategoriesByDepartmentIdAsync(DepartmentId)); } } } diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/PersonnelController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/PersonnelController.cs index 30f88c17..78d39ebf 100644 --- a/Web/Resgrid.WebCore/Areas/User/Controllers/PersonnelController.cs +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/PersonnelController.cs @@ -23,6 +23,8 @@ using Resgrid.Model.Identity; using Resgrid.WebCore.Areas.User.Models.Personnel; using IdentityUser = Resgrid.Model.Identity.IdentityUser; +using Resgrid.WebCore.Areas.User.Models; +using System.Web; namespace Resgrid.Web.Areas.User.Controllers { @@ -48,12 +50,13 @@ public class PersonnelController : SecureBaseController private readonly IGeoService _geoService; private readonly UserManager _userManager; private readonly IDepartmentSettingsService _departmentSettingsService; + private readonly ICallsService _callsService; public PersonnelController(IDepartmentsService departmentsService, IUsersService usersService, IActionLogsService actionLogsService, IEmailService emailService, IUserProfileService userProfileService, IDeleteService deleteService, Model.Services.IAuthorizationService authorizationService, ILimitsService limitsService, IPersonnelRolesService personnelRolesService, IDepartmentGroupsService departmentGroupsService, IUserStateService userStateService, IEventAggregator eventAggregator, IEmailMarketingProvider emailMarketingProvider, ICertificationService certificationService, ICustomStateService customStateService, - IGeoService geoService, UserManager userManager, IDepartmentSettingsService departmentSettingsService) + IGeoService geoService, UserManager userManager, IDepartmentSettingsService departmentSettingsService, ICallsService callsService) { _departmentsService = departmentsService; _usersService = usersService; @@ -73,6 +76,7 @@ public PersonnelController(IDepartmentsService departmentsService, IUsersService _geoService = geoService; _userManager = userManager; _departmentSettingsService = departmentSettingsService; + _callsService = callsService; } #endregion Private Members and Constructors @@ -82,11 +86,201 @@ public async Task Index() PersonnelModel model = new PersonnelModel(); model.LastActivityDates = new Dictionary(); model.States = new Dictionary(); - model.Groups = new Dictionary(); + model.Groups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(DepartmentId); //new Dictionary(); model.CanAddNewUser = await _limitsService.CanDepartmentAddNewUserAsync(DepartmentId); model.CanGroupAdminsAdd = await _authorizationService.CanGroupAdminsAddUsersAsync(DepartmentId); + var personnelStates = await _customStateService.GetActivePersonnelStateForDepartmentAsync(DepartmentId); + var personnelStaffing = await _customStateService.GetActiveStaffingLevelsForDepartmentAsync(DepartmentId); + + if (personnelStates != null) + { + model.PersonnelCustomStatusesId = personnelStates.CustomStateId; + model.PersonnelStates = personnelStates.Details.ToList(); + } + else + model.PersonnelStates = _customStateService.GetDefaultPersonStatuses(); + + if (personnelStaffing != null) + { + model.PersonnelCustomStaffingId = personnelStaffing.CustomStateId; + model.PersonnelStaffings = personnelStaffing.Details.ToList(); + } + else + model.PersonnelStaffings = _customStateService.GetDefaultPersonStaffings(); + + List personnelJson = new List(); + var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); + var users = await _departmentsService.GetAllUsersForDepartmentUnlimitedAsync(DepartmentId); + var departmentMembers = await _departmentsService.GetAllMembersForDepartmentUnlimitedAsync(DepartmentId); + var actionLogs = await _actionLogsService.GetLastActionLogsForDepartmentAsync(DepartmentId); + var staffings = await _userStateService.GetLatestStatesForDepartmentAsync(DepartmentId); + var canGroupAdminsDelete = await _authorizationService.CanGroupAdminsRemoveUsersAsync(DepartmentId); + var profiles = await _userProfileService.GetAllProfilesForDepartmentIncDisabledDeletedAsync(DepartmentId); + var userGroupRoles = await _usersService.GetUserGroupAndRolesByDepartmentIdAsync(DepartmentId, true, true, false); + + var sortOrder = await _departmentSettingsService.GetDepartmentPersonnelSortOrderAsync(DepartmentId); + + foreach (var user in users) + { + var person = new PersonnelForListJson(); + person.UserId = user.UserId.ToString(); + + var member = departmentMembers.FirstOrDefault(x => x.UserId == user.UserId); + var actionLog = actionLogs.FirstOrDefault(x => x.UserId == user.UserId); + var staffing = staffings.FirstOrDefault(x => x.UserId == user.UserId); + + if (!profiles.ContainsKey(user.UserId)) + { + person.Name = "Unknown User"; + } + else + { + var userProfile = profiles[user.UserId]; + person.Name = userProfile.FullName.AsFirstNameLastName; + person.FirstName = userProfile.FirstName; + person.LastName = userProfile.LastName; + } + + if (ClaimsAuthorizationHelper.CanViewPII()) + person.EmailAddress = user.Email; + else + person.EmailAddress = ""; + + var group = await _departmentGroupsService.GetGroupForUserAsync(user.UserId, DepartmentId); + if (group != null) + { + person.Group = group.Name; + + if (department.IsUserAnAdmin(UserId) || (canGroupAdminsDelete && group.IsUserGroupAdmin(UserId) && !department.IsUserAnAdmin(user.UserId))) + person.CanRemoveUser = true; + + if (group.IsUserGroupAdmin(UserId) || department.IsUserAnAdmin(UserId)) + person.CanEditUser = true; + + person.GroupId = group.DepartmentGroupId; + } + else + { + if (department.IsUserAnAdmin(UserId)) + { + person.CanRemoveUser = true; + person.CanEditUser = true; + } + } + + var userGroupRole = userGroupRoles.FirstOrDefault(x => x.UserId == user.UserId); + if (userGroupRole != null) + person.Roles = userGroupRole.RoleNames; + else + person.Roles = ""; + + StringBuilder sb = new StringBuilder(); + + if (member != null) + { + if (member.IsAdmin.HasValue && member.IsAdmin.Value || + department.ManagingUserId == user.UserId) + sb.Append("Admin"); + else + sb.Append("Normal"); + + if (member.IsDisabled.HasValue && member.IsDisabled.Value) + sb.Append(sb.Length > 0 ? ", Disabled" : "Disabled"); + + if (member.IsHidden.HasValue && member.IsHidden.Value) + sb.Append(sb.Length > 0 ? ", Hidden" : "Hidden"); + + person.State = sb.ToString(); + } + else + { + person.State = "Normal"; + } + + if (actionLog != null) + { + person.StatusId = actionLog.ActionTypeId; + } + + if (staffing != null) + { + person.StaffingId = staffing.State; + } + + //if (actionLog != null) + //{ + // person.LastActivityDate = actionLog.Timestamp.TimeConverterToString(department); + //} + //else + //{ + // person.LastActivityDate = "Never"; + //} + + personnelJson.Add(person); + } + + switch (sortOrder) + { + case PersonnelSortOrders.Default: + model.Persons = personnelJson; + break; + case PersonnelSortOrders.FirstName: + model.Persons = personnelJson.OrderBy(x => x.FirstName).ToList(); + break; + case PersonnelSortOrders.LastName: + model.Persons = personnelJson.OrderBy(x => x.LastName).ToList(); + break; + case PersonnelSortOrders.Group: + model.Persons = personnelJson.OrderBy(x => x.GroupId).ToList(); + break; + default: + model.Persons = personnelJson; + break; + } + + List trees = new List(); + var tree0 = new BSTreeModel(); + tree0.id = "TreeGroup_-1"; + tree0.text = "All Personnel"; + tree0.icon = ""; + trees.Add(tree0); + + var tree1 = new BSTreeModel(); + tree1.id = "TreeGroup_0"; + tree1.text = "Ungrouped Personnel"; + tree1.icon = ""; + trees.Add(tree1); + + if (model.Groups != null && model.Groups.Any()) + { + foreach (var topLevelGroup in model.Groups.Where(x => !x.ParentDepartmentGroupId.HasValue).ToList()) + { + var group = new BSTreeModel(); + group.id = $"TreeGroup_{topLevelGroup.DepartmentGroupId.ToString()}"; + group.text = topLevelGroup.Name; + group.icon = ""; + + if (topLevelGroup.Children != null && topLevelGroup.Children.Any()) + { + foreach (var secondLevelGroup in topLevelGroup.Children) + { + var secondLevelGroupTree = new BSTreeModel(); + secondLevelGroupTree.id = $"TreeGroup_{secondLevelGroup.DepartmentGroupId.ToString()}"; + secondLevelGroupTree.text = secondLevelGroup.Name; + secondLevelGroupTree.icon = ""; + + group.nodes.Add(secondLevelGroupTree); + } + } + + trees.Add(group); + } + } + model.TreeData = Newtonsoft.Json.JsonConvert.SerializeObject(trees); + + return View(model); } @@ -893,6 +1087,271 @@ public async Task GetPersonnelListPaged(int perPage, int page) return Json(result); } + [HttpGet] + [Authorize(Policy = ResgridResources.Personnel_View)] + public async Task GetPersonnelStatusHtmlForDropdownByStateId(int customStateId) + { + string buttonHtml = string.Empty; + + CustomState customStates = null; + List activeDetails = null; + + if (customStateId > 25) + { + customStates = await _customStateService.GetCustomSateByIdAsync(customStateId); + + if (customStates != null) + { + activeDetails = customStates.GetActiveDetails(); + } + } + + if (activeDetails == null) + activeDetails = _customStateService.GetDefaultPersonStatuses(); + + StringBuilder sb = new StringBuilder(); + + foreach (var state in activeDetails.OrderBy(x => x.Order)) + { + sb.Append($""); + } + + buttonHtml = sb.ToString(); + + + return Content(buttonHtml); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Personnel_View)] + public async Task GetPersonnelStaffingHtmlForDropdownByStateId(int customStateId) + { + string buttonHtml = string.Empty; + + CustomState customStates = null; + List activeDetails = null; + + if (customStateId > 25) + { + customStates = await _customStateService.GetCustomSateByIdAsync(customStateId); + + if (customStates != null) + { + activeDetails = customStates.GetActiveDetails(); + } + } + + if (activeDetails == null) + activeDetails = _customStateService.GetDefaultPersonStaffings(); + + StringBuilder sb = new StringBuilder(); + + foreach (var state in activeDetails.OrderBy(x => x.Order)) + { + sb.Append($""); + } + + buttonHtml = sb.ToString(); + + + return Content(buttonHtml); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Personnel_View)] + public async Task GetPersonnelStatusDestinationHtmlForDropdown(int customStateId, int customStatusDetailId) + { + string buttonHtml = string.Empty; + + CustomState customStates = null; + List activeDetails = null; + + if (customStateId > 25) + { + customStates = await _customStateService.GetCustomSateByIdAsync(customStateId); + + if (customStates != null) + { + activeDetails = customStates.GetActiveDetails(); + } + } + + if (activeDetails == null) + activeDetails = _customStateService.GetDefaultPersonStatuses(); + + var state = activeDetails.FirstOrDefault(x => x.CustomStateDetailId == customStatusDetailId); + var activeCalls = await _callsService.GetActiveCallsByDepartmentAsync(DepartmentId); + var stations = await _departmentGroupsService.GetAllStationGroupsForDepartmentAsync(DepartmentId); + StringBuilder sb = new StringBuilder(); + + sb.Append($""); + + if (state != null) + { + // No custom drop down options for responding to avoid confusion between Responding Station and Responding Scene + if (customStatusDetailId != (int)ActionTypes.Responding) + { + if (state.DetailType == (int)CustomStateDetailTypes.None) + { + + } + else if (state.DetailType == (int)CustomStateDetailTypes.Calls) + { + foreach (var call in activeCalls) + { + sb.Append($""); + } + } + else if (state.DetailType == (int)CustomStateDetailTypes.Stations) + { + foreach (var station in stations) + { + sb.Append($""); + } + + sb.Append(""); + } + else if (state.DetailType == (int)CustomStateDetailTypes.CallsAndStations) + { + foreach (var call in activeCalls) + { + sb.Append($""); + } + + foreach (var station in stations) + { + sb.Append($""); + } + } + } + } + + buttonHtml = sb.ToString(); + + + return Content(buttonHtml); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Personnel_View)] + public async Task SetActionForUser(string userId, int actionType, int destination, string note, CancellationToken cancellationToken) + { + if (!await _authorizationService.CanUserViewPersonAsync(UserId, userId, DepartmentId)) + Unauthorized(); + + var status = new ActionLog(); + status.UserId = userId; + status.Timestamp = DateTime.UtcNow; + status.ActionTypeId = actionType; + status.DepartmentId = DepartmentId; + + if (destination > 0) + status.DestinationId = destination; + + if (!String.IsNullOrWhiteSpace(note)) + status.Note = HttpUtility.UrlDecode(note); + + try + { + var savedState = await _actionLogsService.SaveActionLogAsync(status, cancellationToken); + } + catch (Exception ex) + { + Logging.LogException(ex); + } + + return RedirectToAction("Index"); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Personnel_View)] + public async Task SetUserActionForMultiple(string userIds, int actionType, int destination, string note, CancellationToken cancellationToken) + { + if (!String.IsNullOrWhiteSpace(userIds) && userIds.Split(char.Parse("|")).Any()) + { + foreach (var userId in userIds.Split(char.Parse("|"))) + { + if (!await _authorizationService.CanUserViewPersonAsync(UserId, userId, DepartmentId)) + Unauthorized(); + + var status = new ActionLog(); + status.UserId = userId; + status.Timestamp = DateTime.UtcNow; + status.ActionTypeId = actionType; + status.DepartmentId = DepartmentId; + + if (destination > 0) + status.DestinationId = destination; + + if (!String.IsNullOrWhiteSpace(note)) + status.Note = HttpUtility.UrlDecode(note); + + try + { + var savedState = await _actionLogsService.SaveActionLogAsync(status, cancellationToken); + } + catch (Exception ex) + { + Logging.LogException(ex); + } + } + } + + return RedirectToAction("Index"); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Personnel_View)] + public async Task SetStaffingForUser(string userId, int staffing, string note, CancellationToken cancellationToken) + { + if (!await _authorizationService.CanUserViewPersonAsync(UserId, userId, DepartmentId)) + Unauthorized(); + + string staffingNote = String.Empty; + if (!String.IsNullOrWhiteSpace(note)) + staffingNote = HttpUtility.UrlDecode(note); + + try + { + var savedState = await _userStateService.CreateUserStateAsync(userId, DepartmentId, staffing, staffingNote, DateTime.UtcNow, cancellationToken); + } + catch (Exception ex) + { + Logging.LogException(ex); + } + + return RedirectToAction("Index"); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Personnel_View)] + public async Task SetUserStaffingForMultiple(string userIds, int staffing, string note, CancellationToken cancellationToken) + { + if (!String.IsNullOrWhiteSpace(userIds) && userIds.Split(char.Parse("|")).Any()) + { + string staffingNote = String.Empty; + if (!String.IsNullOrWhiteSpace(note)) + staffingNote = HttpUtility.UrlDecode(note); + + foreach (var userId in userIds.Split(char.Parse("|"))) + { + if (!await _authorizationService.CanUserViewPersonAsync(UserId, userId, DepartmentId)) + Unauthorized(); + + try + { + var savedState = await _userStateService.CreateUserStateAsync(userId, DepartmentId, staffing, staffingNote, DateTime.UtcNow, cancellationToken); + } + catch (Exception ex) + { + Logging.LogException(ex); + } + } + } + + return RedirectToAction("Index"); + } + [HttpGet] [Authorize(Policy = ResgridResources.Personnel_View)] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/ReportsController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/ReportsController.cs index 6f5954bd..c23a653f 100644 --- a/Web/Resgrid.WebCore/Areas/User/Controllers/ReportsController.cs +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/ReportsController.cs @@ -624,68 +624,72 @@ private async Task CreatePersonnelReportModel(int departmen foreach (var user in users) { - var departmentUser = await _departmentsService.GetDepartmentMemberAsync(user.UserId, DepartmentId); - var person = new PersonnelReportRow(); - var group = await _departmentGroupsService.GetGroupForUserAsync(user.UserId, DepartmentId); - var savedProfile = await _userProfileService.GetProfileByUserIdAsync(user.UserId); + var departmentUser = await _departmentsService.GetDepartmentMemberAsync(user.UserId, DepartmentId, false); - if (departmentUser.IsAdmin.HasValue && departmentUser.IsAdmin.Value || - model.Department.ManagingUserId == user.UserId) - person.DepartmentRole = "Admin"; - else - person.DepartmentRole = "Available"; + if (departmentUser != null) + { + var person = new PersonnelReportRow(); + var group = await _departmentGroupsService.GetGroupForUserAsync(user.UserId, DepartmentId); + var savedProfile = await _userProfileService.GetProfileByUserIdAsync(user.UserId); - if (group != null) - person.Group = group.Name; + if (departmentUser.IsAdmin.HasValue && departmentUser.IsAdmin.Value || + model.Department.ManagingUserId == user.UserId) + person.DepartmentRole = "Admin"; + else + person.DepartmentRole = "Available"; - person.Email = user.Email; - person.Username = user.UserName; + if (group != null) + person.Group = group.Name; - var sb = new StringBuilder(); - var roles = await _personnelRolesService.GetRolesForUserAsync(user.UserId, DepartmentId); - foreach (var role in roles) - { - if (sb.Length > 0) - sb.Append(", "); + person.Email = user.Email; + person.Username = user.UserName; - sb.Append(role.Name); - } + var sb = new StringBuilder(); + var roles = await _personnelRolesService.GetRolesForUserAsync(user.UserId, DepartmentId); + foreach (var role in roles) + { + if (sb.Length > 0) + sb.Append(", "); - person.Roles = sb.ToString(); + sb.Append(role.Name); + } - if (savedProfile != null) - { - person.Name = savedProfile.FullName.AsFirstNameLastName; - person.ID = savedProfile.IdentificationNumber; - person.MobilePhoneNumber = savedProfile.MobileNumber; + person.Roles = sb.ToString(); - if (savedProfile.MailingAddressId.HasValue) + if (savedProfile != null) { - var mailingAddress = - await _addressService.GetAddressByIdAsync(savedProfile.MailingAddressId.Value); - - StringBuilder address = new StringBuilder(); - address.Append("
"); - address.Append(mailingAddress.Address1); - address.Append(" 
"); - address.Append(mailingAddress.City); - address.Append(mailingAddress.State); - address.Append(mailingAddress.PostalCode); - address.Append(" 
"); - address.Append(mailingAddress.Country); - address.Append(" 
"); - address.Append("
"); - - person.MailingAddress = address.ToString(); + person.Name = savedProfile.FullName.AsFirstNameLastName; + person.ID = savedProfile.IdentificationNumber; + person.MobilePhoneNumber = savedProfile.MobileNumber; + + if (savedProfile.MailingAddressId.HasValue) + { + var mailingAddress = + await _addressService.GetAddressByIdAsync(savedProfile.MailingAddressId.Value); + + StringBuilder address = new StringBuilder(); + address.Append("
"); + address.Append(mailingAddress.Address1); + address.Append(" 
"); + address.Append(mailingAddress.City); + address.Append(mailingAddress.State); + address.Append(mailingAddress.PostalCode); + address.Append(" 
"); + address.Append(mailingAddress.Country); + address.Append(" 
"); + address.Append("
"); + + person.MailingAddress = address.ToString(); + } + } + else + { + var userProfile = await _userProfileService.GetProfileByUserIdAsync(user.UserId); + person.Name = userProfile.FullName.AsFirstNameLastName; } - } - else - { - var userProfile = await _userProfileService.GetProfileByUserIdAsync(user.UserId); - person.Name = userProfile.FullName.AsFirstNameLastName; - } - model.Rows.Add(person); + model.Rows.Add(person); + } } return model; @@ -793,89 +797,94 @@ private async Task UpcomingShiftReadinessReportModel var department = await _departmentsService.GetDepartmentByIdAsync(departmentId, false); model.RunOn = DateTime.UtcNow.TimeConverter(department); - foreach (var shift in shifts) + foreach (var s1 in shifts) { - bool readyShift = true; - var shiftRow = new UpcomingShiftReadinessReportRow(); - var nextShiftDay = (from s in shift.Days - where s.Day > DateTime.UtcNow.TimeConverter(department) - orderby s.Day.Day descending - select s).FirstOrDefault(); - - if (nextShiftDay != null) - { - var shiftRoleDeltas = await _shiftsService.GetShiftDayNeedsAsync(nextShiftDay.ShiftDayId); + var shift = await _shiftsService.PopulateShiftData(s1, true, true, true, true, true); - shiftRow.ShiftName = shift.Name; - shiftRow.ShiftDate = nextShiftDay.Day.ToShortDateString(); - shiftRow.Type = ((ShiftAssignmentTypes)shift.AssignmentType).ToString(); - - foreach (var group in shift.Groups) + if (shift != null && shift.Days != null && shift.Days.Count > 0) + { + bool readyShift = true; + var shiftRow = new UpcomingShiftReadinessReportRow(); + var nextShiftDay = (from s in shift.Days + where s.Day > DateTime.UtcNow.TimeConverter(department) + orderby s.Day.Day descending + select s).FirstOrDefault(); + + if (nextShiftDay != null) { - var shiftSubRow = new UpcomingShiftReadinessReportSubRow(); - shiftSubRow.GroupName = group.DepartmentGroup.Name; + var shiftRoleDeltas = await _shiftsService.GetShiftDayNeedsAsync(nextShiftDay.ShiftDayId); - foreach (var role in group.Roles) + shiftRow.ShiftName = shift.Name; + shiftRow.ShiftDate = nextShiftDay.Day.ToShortDateString(); + shiftRow.Type = ((ShiftAssignmentTypes)shift.AssignmentType).ToString(); + + foreach (var group in shift.Groups) { - var subRowRoles = new UpcomingShiftReadinessGroupRole(); - subRowRoles.Name = role.Role.Name; - subRowRoles.Required = role.Required; - subRowRoles.Optional = role.Optional; + var shiftSubRow = new UpcomingShiftReadinessReportSubRow(); + shiftSubRow.GroupName = group.DepartmentGroup.Name; - if (shiftRoleDeltas != null && shiftRoleDeltas.ContainsKey(group.DepartmentGroupId)) + foreach (var role in group.Roles) { - var roleDelta = shiftRoleDeltas[group.DepartmentGroupId]; - - if (roleDelta.ContainsKey(role.PersonnelRoleId)) - subRowRoles.Delta = roleDelta[role.PersonnelRoleId]; + var subRowRoles = new UpcomingShiftReadinessGroupRole(); + subRowRoles.Name = role.Role.Name; + subRowRoles.Required = role.Required; + subRowRoles.Optional = role.Optional; - if (subRowRoles.Delta > 0) - readyShift = false; - } - else - { - subRowRoles.Delta = 0; - } + if (shiftRoleDeltas != null && shiftRoleDeltas.ContainsKey(group.DepartmentGroupId)) + { + var roleDelta = shiftRoleDeltas[group.DepartmentGroupId]; - shiftSubRow.Roles.Add(subRowRoles); - } + if (roleDelta.ContainsKey(role.PersonnelRoleId)) + subRowRoles.Delta = roleDelta[role.PersonnelRoleId]; - foreach (var person in shift.Personnel) - { - var subRowPerson = new UpcomingShiftReadinessPersonnel(); - subRowPerson.Name = await UserHelper.GetFullNameForUser(person.UserId); - subRowPerson.Roles = - (await _personnelRolesService.GetRolesForUserAsync(person.UserId, DepartmentId)) - .Select(x => x.Name).ToList(); + if (subRowRoles.Delta > 0) + readyShift = false; + } + else + { + subRowRoles.Delta = 0; + } - shiftSubRow.Personnel.Add(subRowPerson); - } + shiftSubRow.Roles.Add(subRowRoles); + } - var shiftSignupsForDay = - await _shiftsService.GetShiftSignpsForShiftDayAsync(nextShiftDay.ShiftDayId); - if (shiftSignupsForDay != null) - { - foreach (var signup in shiftSignupsForDay.Where(x => - x.DepartmentGroupId.Value == group.DepartmentGroupId)) + foreach (var person in shift.Personnel) { var subRowPerson = new UpcomingShiftReadinessPersonnel(); - subRowPerson.Name = await UserHelper.GetFullNameForUser(signup.UserId); + subRowPerson.Name = await UserHelper.GetFullNameForUser(person.UserId); subRowPerson.Roles = - (await _personnelRolesService.GetRolesForUserAsync(signup.UserId, DepartmentId)) + (await _personnelRolesService.GetRolesForUserAsync(person.UserId, DepartmentId)) .Select(x => x.Name).ToList(); shiftSubRow.Personnel.Add(subRowPerson); } - } - shiftRow.SubRows.Add(shiftSubRow); - } + var shiftSignupsForDay = + await _shiftsService.GetShiftSignpsForShiftDayAsync(nextShiftDay.ShiftDayId); + if (shiftSignupsForDay != null) + { + foreach (var signup in shiftSignupsForDay.Where(x => + x.DepartmentGroupId.Value == group.DepartmentGroupId)) + { + var subRowPerson = new UpcomingShiftReadinessPersonnel(); + subRowPerson.Name = await UserHelper.GetFullNameForUser(signup.UserId); + subRowPerson.Roles = + (await _personnelRolesService.GetRolesForUserAsync(signup.UserId, DepartmentId)) + .Select(x => x.Name).ToList(); + + shiftSubRow.Personnel.Add(subRowPerson); + } + } + + shiftRow.SubRows.Add(shiftSubRow); + } - if (shift.AssignmentType == (int)ShiftAssignmentTypes.Assigned) - readyShift = true; + if (shift.AssignmentType == (int)ShiftAssignmentTypes.Assigned) + readyShift = true; - shiftRow.Ready = readyShift; - model.Rows.Add(shiftRow); + shiftRow.Ready = readyShift; + model.Rows.Add(shiftRow); + } } } @@ -1686,7 +1695,7 @@ private async Task ActiveCallsResourcesReportModel(int dep summary.LoggedOn = call.LoggedOn.TimeConverter(model.Department); summary.Type = call.Type; - var callData = await _callsService.PopulateCallData(call, true, false, false, true, true, true, false); + var callData = await _callsService.PopulateCallData(call, true, false, false, true, true, true, false, false); if (callData != null) { diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/SearchController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/SearchController.cs new file mode 100644 index 00000000..6818eb07 --- /dev/null +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/SearchController.cs @@ -0,0 +1,182 @@ +using Microsoft.AspNetCore.Mvc; +using Resgrid.Model.Services; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Resgrid.Providers.Claims; +using System.Collections.Generic; +using Resgrid.WebCore.Areas.User.Models.Search; +using Newtonsoft.Json; +using System.Linq; + +namespace Resgrid.Web.Areas.User.Controllers +{ + [Area("User")] + public class SearchController : SecureBaseController + { + private readonly Model.Services.IAuthorizationService _authorizationService; + private readonly IDepartmentSettingsService _departmentSettingsService; + + public SearchController(Model.Services.IAuthorizationService authorizationService, IDepartmentSettingsService departmentSettingsService) + { + _authorizationService = authorizationService; + _departmentSettingsService = departmentSettingsService; + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Department_View)] + public async Task GetSearchResults(string query) + { + List allActions = new List(); + List results = null; + + allActions.Add(new SearchResultJson + { + Label = "/Calls", + Summary = "View Calls and Dispatches", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Dispatch/Dashboard" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Personnel", + Summary = "View People (Personnel)", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Personnel" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Units", + Summary = "View Units (Teams or Apparatuses)", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Units" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Mapping", + Summary = "Large Map View which allows filtering and layers", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Mapping" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Shifts", + Summary = "Shifts (Signup, Recurring, Workshift)", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Shifts" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Logs", + Summary = "Logs for activity in the department (Run, Training, Work, Meetings, Callbacks)", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Logs" + }); + + allActions.Add(new SearchResultJson + { + Label = "/NewLog", + Summary = "Create a new Log (i.e. Run Report, Training Log)", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Logs/NewLog" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Reports", + Summary = "Generate reports based on data in the Department", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Reports" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Calendar", + Summary = "Calendar where you can schedule and signup to events, trainings", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Calendar" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Notes", + Summary = "Department notes which are small bits of information", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Notes" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Documents", + Summary = "Upload and Share documents (like pdfs, word docs, excel)", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Documents" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Trainings", + Summary = "Trainings, Study Guides Procedures for people to review", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Trainings" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Inventory", + Summary = "Inventory for your Stations and Units", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Inventory" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Inbox", + Summary = "View your Messages Inbox", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Messages/Inbox" + }); + + allActions.Add(new SearchResultJson + { + Label = "/Profile", + Summary = "View and Edit your own User Profile", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Home/EditUserProfile?UserId=" + UserId + }); + + if (await _authorizationService.CanUserCreateCallAsync(UserId, DepartmentId)) + { + allActions.Add(new SearchResultJson + { + Label = "/NewCall", + Summary = "Create and Dispatch a new Call", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Dispatch/NewCall" + }); + } + + allActions.Add(new SearchResultJson + { + Label = "/ArchivedCalls", + Summary = "View Archived Calls (old Calls)", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Dispatch/ArchivedCalls" + }); + + if (await _authorizationService.CanUserAddNewUserAsync(DepartmentId, UserId)) + { + allActions.Add(new SearchResultJson + { + Label = "/AddPerson", + Summary = "Manually Create a User Account", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Personnel/AddPerson" + }); + + allActions.Add(new SearchResultJson + { + Label = "/ManageInvites", + Summary = "Send Email Invites for users to create their own Accounts", + Url = Config.SystemBehaviorConfig.ResgridBaseUrl + "/User/Department/Invites" + }); + } + + if (string.IsNullOrWhiteSpace(query)) + results = allActions; + else + { + var querySet = query.Trim().ToLower(); + results = allActions.Where(x => x.Label.ToLower().Contains(querySet) || x.Summary.ToLower().Contains(querySet)).ToList(); + } + + return Content(JsonConvert.SerializeObject(results), "application/json");// Json(results); + } + } +} diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/UnitsController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/UnitsController.cs index 082e0ed3..92e7cf58 100644 --- a/Web/Resgrid.WebCore/Areas/User/Controllers/UnitsController.cs +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/UnitsController.cs @@ -21,6 +21,7 @@ using Resgrid.WebCore.Areas.User.Models.Units; using Resgrid.WebCore.Areas.User.Models; using Resgrid.WebCore.Areas.User.Models.Personnel; +using System.Web; namespace Resgrid.Web.Areas.User.Controllers { @@ -108,13 +109,13 @@ public async Task Index() tree0.text = "All Units"; tree0.icon = ""; trees.Add(tree0); - + var tree1 = new BSTreeModel(); tree1.id = "TreeGroup_0"; tree1.text = "Ungrouped Units"; tree1.icon = ""; trees.Add(tree1); - + if (model.Groups != null && model.Groups.Any()) { foreach (var topLevelGroup in model.Groups.Where(x => !x.ParentDepartmentGroupId.HasValue).ToList()) @@ -433,17 +434,24 @@ public async Task SetUnitState(int unitId, int stateType, Cancell [HttpGet] [Authorize(Policy = ResgridResources.Unit_View)] - public async Task SetUnitStateWithDest(int unitId, int stateType, int type, int destination, CancellationToken cancellationToken) + public async Task SetUnitStateWithDest(int unitId, int stateType, int type, int destination, string note, CancellationToken cancellationToken) { if (!await _authorizationService.CanUserViewUnitAsync(UserId, unitId)) Unauthorized(); + var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); + var state = new UnitState(); state.UnitId = unitId; - state.LocalTimestamp = DateTime.UtcNow; + state.LocalTimestamp = DateTimeHelpers.GetLocalDateTime(DateTime.UtcNow, department.TimeZone); state.State = stateType; state.Timestamp = DateTime.UtcNow; - state.DestinationId = destination; + + if (destination > 0) + state.DestinationId = destination; + + if (!String.IsNullOrWhiteSpace(note)) + state.Note = HttpUtility.UrlDecode(note); try { @@ -459,10 +467,12 @@ public async Task SetUnitStateWithDest(int unitId, int stateType, [HttpGet] [Authorize(Policy = ResgridResources.Unit_View)] - public async Task SetUnitStateForMultiple(string unitIds, int stateType, CancellationToken cancellationToken) + public async Task SetUnitStateForMultiple(string unitIds, int stateType, int type, int destination, string note, CancellationToken cancellationToken) { if (!String.IsNullOrWhiteSpace(unitIds) && unitIds.Split(char.Parse("|")).Any()) { + var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); + foreach (var unitId in unitIds.Split(char.Parse("|"))) { var unit = await _unitsService.GetUnitByIdAsync(int.Parse(unitId)); @@ -470,7 +480,27 @@ public async Task SetUnitStateForMultiple(string unitIds, int sta if (await _authorizationService.CanUserViewUnitAsync(UserId, unit.UnitId)) Unauthorized(); - await _unitsService.SetUnitStateAsync(unit.UnitId, stateType, DepartmentId, cancellationToken); + + var state = new UnitState(); + state.UnitId = int.Parse(unitId); + state.LocalTimestamp = DateTimeHelpers.GetLocalDateTime(DateTime.UtcNow, department.TimeZone); + state.State = stateType; + state.Timestamp = DateTime.UtcNow; + + if (destination > 0) + state.DestinationId = destination; + + if (!String.IsNullOrWhiteSpace(note)) + state.Note = HttpUtility.UrlDecode(note); + + try + { + var savedState = await _unitsService.SetUnitStateAsync(state, DepartmentId, cancellationToken); + } + catch (Exception ex) + { + Logging.LogException(ex); + } } } @@ -490,9 +520,11 @@ public async Task SetUnitStateWithDestForMultiple(string unitIds, if (!await _authorizationService.CanUserViewUnitAsync(UserId, unit.UnitId)) Unauthorized(); + var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); + var state = new UnitState(); state.UnitId = unit.UnitId; - state.LocalTimestamp = DateTime.UtcNow; + state.LocalTimestamp = DateTimeHelpers.GetLocalDateTime(DateTime.UtcNow, department.TimeZone); state.State = stateType; state.Timestamp = DateTime.UtcNow; state.DestinationId = destination; @@ -1030,11 +1062,22 @@ public async Task GetUnitsList() { var customState = await CustomStatesHelper.GetCustomUnitState(state); - unitJson.StateId = state.State; - unitJson.State = customState.ButtonText; - unitJson.StateColor = customState.ButtonColor; - unitJson.TextColor = customState.TextColor; - unitJson.Timestamp = state.Timestamp.TimeConverterToString(department); + if (customState != null) + { + unitJson.StateId = state.State; + unitJson.State = customState.ButtonText; + unitJson.StateColor = customState.ButtonColor; + unitJson.TextColor = customState.TextColor; + unitJson.Timestamp = state.Timestamp.TimeConverterToString(department); + } + else + { + unitJson.StateId = state.State; + unitJson.State = "Unknown"; + unitJson.StateColor = "#d1dade"; + unitJson.TextColor = "5E5E5E"; + unitJson.Timestamp = state.Timestamp.TimeConverterToString(department); + } } unitsJson.Add(unitJson); @@ -1112,12 +1155,161 @@ public async Task GetUnitsForCallGrid(string callLat, string call return Json(unitsJson); } + [HttpGet] + [Authorize(Policy = ResgridResources.Unit_View)] + public async Task GetUnitStatusHtmlForDropdown(int unitId) + { + string buttonHtml = string.Empty; + + if (!await _authorizationService.CanUserViewUnitAsync(UserId, unitId)) + Unauthorized(); + + var unit = await _unitsService.GetUnitByIdAsync(unitId); + var type = await _unitsService.GetUnitTypeByNameAsync(DepartmentId, unit.Type); + CustomState customStates = null; + List activeDetails = null; + + if (type != null && type.CustomStatesId.HasValue) + { + customStates = await _customStateService.GetCustomSateByIdAsync(type.CustomStatesId.Value); + + if (customStates != null) + { + activeDetails = customStates.GetActiveDetails(); + } + } + + if (activeDetails == null) + activeDetails = _customStateService.GetDefaultUnitStatuses(); + + StringBuilder sb = new StringBuilder(); + + foreach (var state in activeDetails.OrderBy(x => x.Order)) + { + sb.Append($""); + } + + buttonHtml = sb.ToString(); + + + return Content(buttonHtml); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Unit_View)] + public async Task GetUnitStatusHtmlForDropdownByStateId(int customStateId) + { + string buttonHtml = string.Empty; + + CustomState customStates = null; + List activeDetails = null; + + if (customStateId > 25) + { + customStates = await _customStateService.GetCustomSateByIdAsync(customStateId); + + if (customStates != null) + { + activeDetails = customStates.GetActiveDetails(); + } + } + + if (activeDetails == null) + activeDetails = _customStateService.GetDefaultUnitStatuses(); + + StringBuilder sb = new StringBuilder(); + + foreach (var state in activeDetails.OrderBy(x => x.Order)) + { + sb.Append($""); + } + + buttonHtml = sb.ToString(); + + + return Content(buttonHtml); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Unit_View)] + public async Task GetUnitStatusDestinationHtmlForDropdown(int customStateId, int customStatusDetailId) + { + string buttonHtml = string.Empty; + + CustomState customStates = null; + List activeDetails = null; + + if (customStateId > 25) + { + customStates = await _customStateService.GetCustomSateByIdAsync(customStateId); + + if (customStates != null) + { + activeDetails = customStates.GetActiveDetails(); + } + } + + if (activeDetails == null) + activeDetails = _customStateService.GetDefaultUnitStatuses(); + + var state = activeDetails.FirstOrDefault(x => x.CustomStateDetailId == customStatusDetailId); + var activeCalls = await _callsService.GetActiveCallsByDepartmentAsync(DepartmentId); + var stations = await _departmentGroupsService.GetAllStationGroupsForDepartmentAsync(DepartmentId); + StringBuilder sb = new StringBuilder(); + + sb.Append($""); + + if (state != null) + { + if (state.DetailType == (int)CustomStateDetailTypes.None) + { + + } + else if (state.DetailType == (int)CustomStateDetailTypes.Calls) + { + foreach (var call in activeCalls) + { + sb.Append($""); + } + } + else if (state.DetailType == (int)CustomStateDetailTypes.Stations) + { + foreach (var station in stations) + { + sb.Append($""); + } + + sb.Append(""); + } + else if (state.DetailType == (int)CustomStateDetailTypes.CallsAndStations) + { + foreach (var call in activeCalls) + { + sb.Append($""); + } + + foreach (var station in stations) + { + sb.Append($""); + } + } + } + + buttonHtml = sb.ToString(); + + + return Content(buttonHtml); + } + + [HttpGet] [Authorize(Policy = ResgridResources.Unit_View)] public async Task GetUnitOptionsDropdown(int unitId) { string buttonHtml = string.Empty; + if (!await _authorizationService.CanUserViewUnitAsync(UserId, unitId)) + Unauthorized(); var unit = await _unitsService.GetUnitByIdAsync(unitId); var type = await _unitsService.GetUnitTypeByNameAsync(DepartmentId, unit.Type); diff --git a/Web/Resgrid.WebCore/Areas/User/Controllers/VoiceController.cs b/Web/Resgrid.WebCore/Areas/User/Controllers/VoiceController.cs index cdb796aa..2cff561f 100644 --- a/Web/Resgrid.WebCore/Areas/User/Controllers/VoiceController.cs +++ b/Web/Resgrid.WebCore/Areas/User/Controllers/VoiceController.cs @@ -40,6 +40,11 @@ public async Task Index() model.Voice.Channels = new List(); } + model.Audios = await _voiceService.GetDepartmentAudiosByDepartmentIdAsync(DepartmentId); + + if (model.Audios == null) + model.Audios = new List(); + return View(model); } @@ -167,5 +172,101 @@ public async Task Delete(string id, CancellationToken cancellatio return RedirectToAction("Index"); } + + + [HttpGet] + [Authorize(Policy = ResgridResources.Voice_Create)] + public async Task NewAudio() + { + var model = new NewAudioStreamModel(); + + return View(model); + } + + [HttpPost] + [Authorize(Policy = ResgridResources.Voice_Create)] + public async Task NewAudio(NewAudioStreamModel model, CancellationToken cancellationToken) + { + if (ModelState.IsValid) + { + var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId); + + DepartmentAudio audio = new DepartmentAudio(); + audio.DepartmentId = DepartmentId; + audio.AddedOn = DateTime.UtcNow; + audio.AddedByUserId = UserId; + audio.Name = model.Name; + audio.Data = model.Url; + audio.DepartmentAudioType = 1; + + var savedAudio = await _voiceService.SaveDepartmentAudioAsync(audio, cancellationToken); + + return RedirectToAction("Index"); + } + + return View(model); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Voice_Create)] + public async Task EditAudio(string id) + { + var model = new NewAudioStreamModel(); + + if (String.IsNullOrWhiteSpace(id)) + Unauthorized(); + + var audio = await _voiceService.GetDepartmentAudioByIdAsync(id); + + if (audio == null) + Unauthorized(); + + if (audio.DepartmentId != DepartmentId) + Unauthorized(); + + model.Id = audio.DepartmentAudioId; + model.Name = audio.Name; + model.Url = audio.Data; + + return View(model); + } + + [HttpPost] + [Authorize(Policy = ResgridResources.Voice_Create)] + public async Task EditAudio(NewAudioStreamModel model, CancellationToken cancellationToken) + { + if (ModelState.IsValid) + { + var audio = await _voiceService.GetDepartmentAudioByIdAsync(model.Id); + + if (audio == null) + Unauthorized(); + + if (audio.DepartmentId != DepartmentId) + Unauthorized(); + + audio.Name = model.Name; + audio.Data = model.Url; + + await _voiceService.SaveDepartmentAudioAsync(audio, cancellationToken); + + return RedirectToAction("Index"); + } + + return View(model); + } + + [HttpGet] + [Authorize(Policy = ResgridResources.Voice_Delete)] + public async Task DeleteAudio(string id, CancellationToken cancellationToken) + { + var audio = await _voiceService.GetDepartmentAudioByIdAsync(id); + if (audio != null && audio.DepartmentId == DepartmentId) + { + var result = await _voiceService.DeleteDepartmentAudioAsync(audio, cancellationToken); + } + + return RedirectToAction("Index"); + } } } diff --git a/Web/Resgrid.WebCore/Areas/User/Models/Account/DeleteAccountModel.cs b/Web/Resgrid.WebCore/Areas/User/Models/Account/DeleteAccountModel.cs index 39aee425..7d2e17c3 100644 --- a/Web/Resgrid.WebCore/Areas/User/Models/Account/DeleteAccountModel.cs +++ b/Web/Resgrid.WebCore/Areas/User/Models/Account/DeleteAccountModel.cs @@ -6,5 +6,7 @@ public class DeleteAccountModel : BaseUserModel { [Required] public bool AreYouSure { get; set; } + + public bool IsDepartmentOwner { get; set; } } -} \ No newline at end of file +} diff --git a/Web/Resgrid.WebCore/Areas/User/Models/Calls/NewCallView.cs b/Web/Resgrid.WebCore/Areas/User/Models/Calls/NewCallView.cs index 853e7f7a..e73b8f26 100644 --- a/Web/Resgrid.WebCore/Areas/User/Models/Calls/NewCallView.cs +++ b/Web/Resgrid.WebCore/Areas/User/Models/Calls/NewCallView.cs @@ -31,6 +31,9 @@ public class NewCallView : BaseUserModel public SelectList CallTemplates { get; set; } public int CallTemplateId { get; set; } public string NewCallFormData { get; set; } + public string ClosedCallNotes { get; set; } + public ClosedOnlyCallStates CallState { get; set; } + public SelectList CallStates { get; set; } public NewCallView() { diff --git a/Web/Resgrid.WebCore/Areas/User/Models/Calls/ViewCallView.cs b/Web/Resgrid.WebCore/Areas/User/Models/Calls/ViewCallView.cs index 5d32f3ba..ad51734b 100644 --- a/Web/Resgrid.WebCore/Areas/User/Models/Calls/ViewCallView.cs +++ b/Web/Resgrid.WebCore/Areas/User/Models/Calls/ViewCallView.cs @@ -24,6 +24,7 @@ public class ViewCallView: BaseUserModel public List Units { get; set; } public List Stations { get; set; } public List Protocols { get; set; } + public List ChildCalls { get; set; } public string IsMapTabActive() { diff --git a/Web/Resgrid.WebCore/Areas/User/Models/DepartmentSettingsModel.cs b/Web/Resgrid.WebCore/Areas/User/Models/DepartmentSettingsModel.cs index c335b5ef..f05336d0 100644 --- a/Web/Resgrid.WebCore/Areas/User/Models/DepartmentSettingsModel.cs +++ b/Web/Resgrid.WebCore/Areas/User/Models/DepartmentSettingsModel.cs @@ -32,9 +32,13 @@ public class DepartmentSettingsModel : BaseUserModel public bool EnableStaffingReset { get; set; } public string TimeToResetStaffing { get; set; } public SelectList StaffingLevels { get; set; } + public List Staffings { get; set; } public int ResetStaffingTo { get; set; } public UserStateTypes UserStateTypes { get; set; } + public bool EnableStaffingSupress { get; set; } + public DepartmentSuppressStaffingInfo SuppressStaffingInfo { get; set; } + public bool EnableStatusReset { get; set; } public string TimeToResetStatus { get; set; } public SelectList StatusLevels { get; set; } diff --git a/Web/Resgrid.WebCore/Areas/User/Models/Departments/DeleteDepartmentView.cs b/Web/Resgrid.WebCore/Areas/User/Models/Departments/DeleteDepartmentView.cs new file mode 100644 index 00000000..22ff684e --- /dev/null +++ b/Web/Resgrid.WebCore/Areas/User/Models/Departments/DeleteDepartmentView.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.Departments; + +public class DeleteDepartmentView +{ + public Department Department { get; set; } + public QueueItem CurrentDeleteRequest { get; set; } + public UserProfile Profile { get; set; } + + [Required] + public bool AreYouSure { get; set; } +} diff --git a/Web/Resgrid.WebCore/Areas/User/Models/Dispatch/CallExportView.cs b/Web/Resgrid.WebCore/Areas/User/Models/Dispatch/CallExportView.cs index a308f894..6b632ebc 100644 --- a/Web/Resgrid.WebCore/Areas/User/Models/Dispatch/CallExportView.cs +++ b/Web/Resgrid.WebCore/Areas/User/Models/Dispatch/CallExportView.cs @@ -21,5 +21,6 @@ public class CallExportView : BaseUserModel public string EndLat { get; set; } public string EndLon { get; set; } public List Names { get; set; } + public List ChildCalls { get; set; } } } diff --git a/Web/Resgrid.WebCore/Areas/User/Models/Dispatch/CallSelectListJson.cs b/Web/Resgrid.WebCore/Areas/User/Models/Dispatch/CallSelectListJson.cs new file mode 100644 index 00000000..f8fda3d5 --- /dev/null +++ b/Web/Resgrid.WebCore/Areas/User/Models/Dispatch/CallSelectListJson.cs @@ -0,0 +1,17 @@ + +using System.Collections.Generic; + +namespace Resgrid.WebCore.Areas.User.Models.Dispatch +{ + public class CallSelectListJson + { + public List results { get; set; } + } + + + public class CallSelectListJsonResult + { + public int id { get; set; } + public string text { get; set; } + } +} diff --git a/Web/Resgrid.WebCore/Areas/User/Models/EditProfileModel.cs b/Web/Resgrid.WebCore/Areas/User/Models/EditProfileModel.cs index 44907e09..a1db16af 100644 --- a/Web/Resgrid.WebCore/Areas/User/Models/EditProfileModel.cs +++ b/Web/Resgrid.WebCore/Areas/User/Models/EditProfileModel.cs @@ -25,6 +25,7 @@ public class EditProfileModel: BaseUserModel public bool HasCustomIamge { get; set; } public List UsersRoles { get; set; } public bool IsOwnProfile { get; set; } + public bool IsFreePlan { get; set; } [Required] [MaxLength(50)] diff --git a/Web/Resgrid.WebCore/Areas/User/Models/Personnel/PersonnelForJson.cs b/Web/Resgrid.WebCore/Areas/User/Models/Personnel/PersonnelForJson.cs index 1f4bafc0..bb820023 100644 --- a/Web/Resgrid.WebCore/Areas/User/Models/Personnel/PersonnelForJson.cs +++ b/Web/Resgrid.WebCore/Areas/User/Models/Personnel/PersonnelForJson.cs @@ -28,6 +28,8 @@ public class PersonnelForListJson public string Roles { get; set; } public string LastActivityDate { get; set; } public string State { get; set; } + public int StatusId { get; set; } + public int StaffingId { get; set; } public string UserId { get; set; } public bool CanRemoveUser { get; set; } public bool CanEditUser { get; set; } diff --git a/Web/Resgrid.WebCore/Areas/User/Models/PersonnelModel.cs b/Web/Resgrid.WebCore/Areas/User/Models/PersonnelModel.cs index b81a60b4..7bda37ad 100644 --- a/Web/Resgrid.WebCore/Areas/User/Models/PersonnelModel.cs +++ b/Web/Resgrid.WebCore/Areas/User/Models/PersonnelModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Resgrid.Model; using Resgrid.Model.Identity; +using Resgrid.Web.Areas.User.Models.Personnel; namespace Resgrid.Web.Areas.User.Models { @@ -12,8 +13,16 @@ public class PersonnelModel: BaseUserModel public List Users { get; set; } public Dictionary LastActivityDates { get; set; } public Dictionary States { get; set; } - public Dictionary Groups { get; set; } + //public Dictionary Groups { get; set; } public bool CanAddNewUser { get; set; } public bool CanGroupAdminsAdd { get; set; } - } + + public List Persons { get; set; } + public List PersonnelStates { get; set; } + public int PersonnelCustomStatusesId { get; set; } + public List PersonnelStaffings { get; set; } + public int PersonnelCustomStaffingId { get; set; } + public List Groups { get; set; } + public string TreeData { get; set; } + } } diff --git a/Web/Resgrid.WebCore/Areas/User/Models/Search/SearchResultJson.cs b/Web/Resgrid.WebCore/Areas/User/Models/Search/SearchResultJson.cs new file mode 100644 index 00000000..e99d2a94 --- /dev/null +++ b/Web/Resgrid.WebCore/Areas/User/Models/Search/SearchResultJson.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Resgrid.WebCore.Areas.User.Models.Search +{ + public class SearchResultJson + { + [JsonProperty(PropertyName = "label")] + public string Label { get; set; } + + [JsonProperty(PropertyName = "summary")] + public string Summary { get; set; } + + [JsonProperty(PropertyName = "url")] + public string Url { get; set; } + } +} diff --git a/Web/Resgrid.WebCore/Areas/User/Models/Voice/NewAudioStreamModel.cs b/Web/Resgrid.WebCore/Areas/User/Models/Voice/NewAudioStreamModel.cs new file mode 100644 index 00000000..653fb656 --- /dev/null +++ b/Web/Resgrid.WebCore/Areas/User/Models/Voice/NewAudioStreamModel.cs @@ -0,0 +1,17 @@ +using Resgrid.Model; +using System.ComponentModel.DataAnnotations; + +namespace Resgrid.WebCore.Areas.User.Models.Voice +{ + public class NewAudioStreamModel + { + public string Id { get; set; } + public string Message { get; set; } + + [Required] + public string Name { get; set; } + + [Required] + public string Url { get; set; } + } +} diff --git a/Web/Resgrid.WebCore/Areas/User/Models/Voice/VoiceIndexModel.cs b/Web/Resgrid.WebCore/Areas/User/Models/Voice/VoiceIndexModel.cs index 3a7482aa..67f9aee0 100644 --- a/Web/Resgrid.WebCore/Areas/User/Models/Voice/VoiceIndexModel.cs +++ b/Web/Resgrid.WebCore/Areas/User/Models/Voice/VoiceIndexModel.cs @@ -1,4 +1,5 @@ using Resgrid.Model; +using System.Collections.Generic; namespace Resgrid.WebCore.Areas.User.Models.Voice { @@ -6,5 +7,6 @@ public class VoiceIndexModel { public bool CanUseVoice { get; set; } public DepartmentVoice Voice { get; set; } + public List Audios { get; set; } } } diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Account/DeleteAccount.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Account/DeleteAccount.cshtml index 38f97227..b6498452 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Account/DeleteAccount.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Account/DeleteAccount.cshtml @@ -1,94 +1,100 @@ - -@model Resgrid.Web.Areas.User.Models.Account.DeleteAccountModel +@model Resgrid.Web.Areas.User.Models.Account.DeleteAccountModel +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Delete Person"; - Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; + ViewBag.Title = "Resgrid | " + @localizer["DeleteAccountHeader"]; } -
-
- - -
-
-
-
-
-
- -
-
- @using (Html.BeginForm("DeletePerson", "Personnel", FormMethod.Post, new { area = "User", id = "delete_person", @class = "form-horizontal" })) - { - if (!ViewData.ModelState.IsValid) - { -
- @Html.ValidationSummary(false, "Account delete was unsuccessful. Please correct the errors and try again.") -
- } -
- @Html.AntiForgeryToken() -
- -
- Are you sure you want to delete your account? If you choose to delete this account the information below will be permanently deleted. The recommended procedure is to disable the user if you leave the department or organization. The department manager or another department admin can disable the account. Only delete the user after enough time has gone by, the account was a mistake or never used. -

- Data that is Deleted -
    -
  • User Account
  • -
  • Action History
  • -
  • State History
  • -
  • Call and work logs
  • -
  • Unit logs
  • -
  • Messages Sent or Received
  • -
- - Data that is Altered -
    -
  • Calls where they are the reporting user or closing user is mapped to the Departments Managing User
  • -
-
-
-
- Click the checkbox below to delete this user. -
-
-
-
- -
-
-
-
- +
+
+

@localizer["DeleteAccountHeader"]

+ +
+
+ +
+
+
+
+
+
+ + @Html.AntiForgeryToken() +
+ + +
+ @localizer["DeleteAreYouSure1"] +

+ @localizer["DeleteAreYouSure2"] +

+ @localizer["DeleteAreYouSure3"] +

+
+
+ @if (!Model.IsDepartmentOwner) + { +
+ @localizer["CheckToDelete"] +
- Cancel -
- } -   -
-
-
-
-
+
+ +
+
+ + +
+
+
-
-
-
-
+
+
+ @commonLocalizer["Cancel"] + +
+
+ } + else + { +
+ @localizer["DepartmentOwnerError"] +
+ } + +
+ + + + @section Scripts -{ + { + } diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Edit.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Edit.cshtml index 1ef906df..2ba9567b 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Edit.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Edit.cshtml @@ -1,7 +1,7 @@ - -@model Resgrid.WebCore.Areas.User.Models.Calendar.EditCalendarEntry +@model Resgrid.WebCore.Areas.User.Models.Calendar.EditCalendarEntry +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Edit Calendar Entry"; + ViewBag.Title = "Resgrid | " + @localizer["EditEntryeHeader"]; } @section Styles @@ -11,16 +11,16 @@
-

Edit Calendar Entry

+

@localizer["EditEntryeHeader"]

@@ -55,24 +55,24 @@
-

Warning

- This is the recurrence parent item. If you check the "Apply to all" checkbox that will update all the recurrence items with the data from this item. If you delete this item all child recurrence items will also be deleted. +

@commonLocalizer["Warning"]

+ @localizer["EditEntryWarning"]
}
- -
+ +
- +
- +
@@ -81,13 +81,13 @@
- +
- +
@@ -96,8 +96,8 @@
-

Info

- This is the recurrence child item. You cannot alter recurrence settings for this item. If you need to change a recurrence pattern for this entry you will need to remove it and re-add from the parent entry. +

@commonLocalizer["Info"]

+ @localizer["RecurrenceChildWarning"]
@@ -105,36 +105,36 @@
-

Info

- This is the recurrence parent item. You cannot alter recurrence settings for this item. If you need to change a recurrence pattern you need to first remove then create a new recurrence entry. You can change any of the other details in the entry. +

@commonLocalizer["Info"]

+ @localizer["RecurrenceParentWarning"]
- +
- +
- Leave blank (empty) to never Expire or End + @localizer["EndOnHelp"]
- +
@@ -148,7 +148,7 @@
- +
@@ -162,7 +162,7 @@
- +
@@ -176,7 +176,7 @@
- +
@@ -190,7 +190,7 @@
- +
@@ -204,7 +204,7 @@
- +
@@ -218,7 +218,7 @@
- +
@@ -235,13 +235,13 @@
  • @@ -254,22 +254,22 @@ @@ -278,7 +278,7 @@
- +
@@ -288,7 +288,7 @@
- +
@*
@@ -310,49 +310,49 @@
*@
- +
- +
- +
@@ -360,8 +360,8 @@
- Cancel - + @commonLocalizer["Cancel"] +
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/EditType.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/EditType.cshtml index b01f84b4..7611ef40 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/EditType.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/EditType.cshtml @@ -1,7 +1,7 @@ - -@model Resgrid.Web.Areas.User.Models.Calendar.NewTypeView +@model Resgrid.Web.Areas.User.Models.Calendar.NewTypeView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Edit Calendar Type"; + ViewBag.Title = "Resgrid | " + @localizer["EditCalendarTypeHeader"]; } @section Styles @@ -11,19 +11,19 @@
-

Edit Calendar Type

+

@localizer["EditCalendarTypeHeader"]

@@ -53,18 +53,18 @@
- +
- +
- Cancel - + @localizer["Cancel"] +
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Index.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Index.cshtml index 22cfce0d..6af6d716 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Index.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Index.cshtml @@ -1,124 +1,125 @@ @using Resgrid.Model.Helpers @using Resgrid.Web.Helpers @model Resgrid.Web.Areas.User.Models.Calendar.IndexView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Calendar"; - Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; + ViewBag.Title = "Resgrid | " + @localizer["CalendarHeader"]; + Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; } @section Styles - { - + { + } -
-
-

Calendar

- -
-
-
-
- @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) - { - Manage Types - } - @if (ClaimsAuthorizationHelper.CanCreateCalendarEntry()) - { - New Entry - } -
+
+
+

@localizer["CalendarHeader"]

+ +
+
+
+
+ @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) + { + @localizer["ManageTypes"] + } + @if (ClaimsAuthorizationHelper.CanCreateCalendarEntry()) + { + @localizer["NewEntry"] + } +
+
-
-
-
-
-
-
-
-
-
-
-
-
-
Upcoming
+
+
+
+
+
+
+
-
-
- @if (Model.UpcomingItems.Any()) - { - foreach (var item in Model.UpcomingItems) - { +
+
+
+
@localizer["Upcoming"]
+
+
+
+ @if (Model.UpcomingItems.Any()) + { + foreach (var item in Model.UpcomingItems) + { - var itemType = Model.Types.FirstOrDefault(x => x.CalendarItemTypeId == item.ItemType); + var itemType = Model.Types.FirstOrDefault(x => x.CalendarItemTypeId == item.ItemType); - if (itemType != null) - { - - } - else - { - - } - } - } - else - { -
- No Upcoming Items -
- } -
-
-
-
-
-
Types
-
-
-
- @if (Model.Types.Any()) - { - foreach (var type in Model.Types) - { -
@type.Name
- } - } - else - { -
- No Custom Types -
- } -
+ if (itemType != null) + { + + } + else + { + + } + } + } + else + { +
+ @localizer["NoUpcomingItems"] +
+ } +
+
+
+
+
+
@localizer["Types"]
+
+
+
+ @if (Model.Types.Any()) + { + foreach (var type in Model.Types) + { +
@type.Name
+ } + } + else + { +
+ @localizer["NoCustomTypes"] +
+ } +
+
+
-
-
@section Scripts - { - + { + } diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/New.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/New.cshtml index 3b103be9..d901f15e 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/New.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/New.cshtml @@ -1,7 +1,7 @@ - -@model Resgrid.WebCore.Areas.User.Models.Calendar.NewCalendarEntry +@model Resgrid.WebCore.Areas.User.Models.Calendar.NewCalendarEntry +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | New Calendar Entry"; + ViewBag.Title = "Resgrid | " + @localizer["NewCalendarEntryHeader"]; } @section Styles @@ -11,16 +11,16 @@
-

New Calendar Entry

+

@localizer["NewCalendarEntryHeader"]

@@ -49,11 +49,11 @@
- -
+ +
- +
@@ -69,33 +69,33 @@
- +
- +
- +
- +
- Leave blank (empty) to never Expire or End + @localizer["EndOnHelp"]
@@ -104,7 +104,7 @@
- +
@@ -118,7 +118,7 @@
- +
@@ -132,7 +132,7 @@
- +
@@ -146,7 +146,7 @@
- +
@@ -160,7 +160,7 @@
- +
@@ -174,7 +174,7 @@
- +
@@ -188,7 +188,7 @@
- +
@@ -205,13 +205,13 @@
  • @@ -225,22 +225,22 @@   @@ -249,7 +249,7 @@
- +
@@ -259,7 +259,7 @@
- +
@*
@@ -281,49 +281,49 @@
*@
- +
- +
- +
@@ -331,8 +331,8 @@
- Cancel - + @commonLocalizer["Cancel"] +
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/NewType.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/NewType.cshtml index a8d5ef96..24b897ee 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/NewType.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/NewType.cshtml @@ -1,7 +1,7 @@ - -@model Resgrid.Web.Areas.User.Models.Calendar.NewTypeView +@model Resgrid.Web.Areas.User.Models.Calendar.NewTypeView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | New Calendar Type"; + ViewBag.Title = "Resgrid | " + @localizer["NewCalendarTypeHeader"]; } @section Styles @@ -11,19 +11,19 @@
-

New Calendar Type

+

@localizer["NewCalendarTypeHeader"]

@@ -52,18 +52,18 @@
- +
- +
- Cancel - + @localizer["Cancel"] +
@@ -84,4 +84,4 @@ }); -} \ No newline at end of file +} diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Types.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Types.cshtml index 931bf625..efba8967 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Types.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/Types.cshtml @@ -1,85 +1,86 @@ @model Resgrid.Web.Areas.User.Models.Calendar.TypesView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Calendar Item Types"; + ViewBag.Title = "Resgrid | Calendar Item Types" + @localizer["CalendarTypesHeader"]; } @section Styles -{ + { } -
-
-

Calendar Item Types

- -
- @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) - { - - } +
+
+

@localizer["CalendarTypesHeader"]

+ +
+ @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) + { + + }
-
-
-
-
-
- - - - - - - - - +
+
+
+
+
+
NameColorActions
+ + + + + + + + - @foreach (var type in Model.Types) - { + @foreach (var type in Model.Types) + { - - - - - - } + + + + + + } - + -
@commonLocalizer["Name"]@commonLocalizer["Color"]@commonLocalizer["Actions"]
- @type.Name - - @type.Color - - @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) - { - Edit - Delete - } -
+ @type.Name + + @type.Color + + @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) + { + @commonLocalizer["Edit"] + @commonLocalizer["Delete"] + } +
-
-
-
-
-
+ +
+
+
+
+
@section Scripts -{ + { } diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/View.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/View.cshtml index 84bd56c8..bbfc5809 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Calendar/View.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Calendar/View.cshtml @@ -3,548 +3,543 @@ @using Resgrid.Model.Helpers @using Resgrid.Web.Helpers @model Resgrid.Web.Areas.User.Models.Calendar.CalendarItemView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Event Detail"; + ViewBag.Title = "Resgrid | " + @localizer["CalendarItemHeader"]; } @section Styles - { + { } - - - -
-
-

Event

- -
- -
-
-
- @if (Model.CanEdit && Model.IsRecurrenceParent == false) - { - Delete Event - } - else if (Model.CanEdit) - { - Delete Event and Occurrences + + + +
+
+

@localizer["CalendarItemHeader"]

+ +
+ +
+
+
+ @if (Model.CanEdit && Model.IsRecurrenceParent == false) + { + @localizer["DeleteEvent"] + } + else if (Model.CanEdit) + { + @localizer["DeleteEventOccurrences"] + } + + @if (Model.CanEdit) + { + @localizer["EditEvent"] + }
-
-
-
-
-

Details

-
-
-
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
Event@Model.CalendarItem.Title
Description@Html.Raw(Model.CalendarItem.Description)
Start@Model.CalendarItem.Start.TimeConverter(Model.Department).ToString("dd MMMM yyyy HH:mm")
End@Model.CalendarItem.End.TimeConverter(Model.Department).ToString("dd MMMM yyyy HH:mm")
Location@Model.CalendarItem.Location
-
+
+
+
+ +
+
+
+
+
+
+
+
    +
  • + +
    +

    @Model.CalendarItem.Title

    +

    @Html.Raw(Model.CalendarItem.Description)

    +

    @Model.CalendarItem.Start.TimeConverter(Model.Department).ToShortTimeString() - @Model.CalendarItem.End.TimeConverter(Model.Department).ToShortTimeString()

    +
    +
  • +
-
-
-
-
-
-
-
-
-
- -
-
-
-

Attendees

-
-
-
- - - - - - - - - - - - @foreach (var attendee in Model.CalendarItem.Attendees) - { - - - - - - - - - - } - -
- Name - - Timestamp - - Type - - Note - - Action -
- @(await UserHelper.GetFullNameForUser(attendee.UserId)) - - @attendee.Timestamp.TimeConverterToString(Model.Department) - - @if (attendee.AttendeeType == 1) - { - RSVP } - else if (attendee.AttendeeType == 1) - { - Required } - else - { - Optional} - - @if (!String.IsNullOrWhiteSpace(attendee.Note)) - { - @attendee.Note } - else - { - No Note} - - @if (attendee.UserId == ClaimsAuthorizationHelper.GetUserId() && Model.CalendarItem.Start >= DateTime.UtcNow) - { - Remove Me } - else if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) - { - Remove} -
+
+
+
+

Details

+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
@commonLocalizer["Name"]@Model.CalendarItem.Title
@commonLocalizer["Description"]@Html.Raw(Model.CalendarItem.Description)
@commonLocalizer["Start"]@Model.CalendarItem.Start.TimeConverter(Model.Department).ToString("dd MMMM yyyy HH:mm")
@commonLocalizer["End"]@Model.CalendarItem.End.TimeConverter(Model.Department).ToString("dd MMMM yyyy HH:mm")
@commonLocalizer["Location"]@Model.CalendarItem.Location
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- @if (Model.CalendarItem.SignupType != 0 && !Model.CalendarItem.IsUserAttending(Model.UserId) && Model.CalendarItem.Start >= DateTime.UtcNow) - { -
-
-
-
+
-
- @using (Html.BeginForm("Signup", "Calendar", FormMethod.Post, new { area = "User", @class = "form-horizontal" })) - { - @Html.HiddenFor(m => m.CalendarItem.CalendarItemId) - -
- -
-
-
- @Html.TextAreaFor(m => m.Note, new { rows = "10", cols = "30", style = "width:450px;height:250px" }) +
+
+

Attendees

+
+
+
+ + + + + + + + + + + + @foreach (var attendee in Model.CalendarItem.Attendees) + { + + + + + + + + + + + } + +
+ @commonLocalizer["Name"] + + @commonLocalizer["Timestamp"] + + @commonLocalizer["Type"] + + @commonLocalizer["Note"] + + @commonLocalizer["Action"] +
+ @(await UserHelper.GetFullNameForUser(attendee.UserId)) + + @attendee.Timestamp.TimeConverterToString(Model.Department) + + @if (attendee.AttendeeType == 1) + { + @localizer["RSVP"] + } + else if (attendee.AttendeeType == 1) + { + @commonLocalizer["Required"] + } + else + { + @commonLocalizer["Optional"] + } + + @if (!String.IsNullOrWhiteSpace(attendee.Note)) + { + @attendee.Note + } + else + { + @localizer["NoNote"] + } + + @if (attendee.UserId == ClaimsAuthorizationHelper.GetUserId() && Model.CalendarItem.Start >= DateTime.UtcNow) + { + @localizer["RemoveMe"] + } + else if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) + { + @commonLocalizer["Remove"] + } +
+
+
-
-
-
- -
- } -
+
-
+ @if (Model.CalendarItem.SignupType != 0 && !Model.CalendarItem.IsUserAttending(Model.UserId) && Model.CalendarItem.Start >= DateTime.UtcNow) + { +
+
+
+
+
+
+ @using (Html.BeginForm("Signup", "Calendar", FormMethod.Post, new { area = "User", @class = "form-horizontal" })) + { + @Html.HiddenFor(m => m.CalendarItem.CalendarItemId) + +
+ +
+
+
+ @Html.TextAreaFor(m => m.Note, new { rows = "10", cols = "30", style = "width:450px;height:250px" }) +
+
+
+
+
+ +
+ } +
+
+
+
+
+
+ }
-
- }
-
-
@section Scripts - { - + { + } diff --git a/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/Edit.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/Edit.cshtml index 07ba75dd..42b8c6f7 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/Edit.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/Edit.cshtml @@ -1,22 +1,23 @@ @using Resgrid.Model @model Resgrid.Web.Areas.User.Models.CustomStatuses.EditStatusView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Edit Custom Statuses"; + ViewBag.Title = "Resgrid | " + @localizer["EditEntryHeader"]; }
-

Edit Custom Statuses

+

@localizer["EditEntryHeader"]

@@ -30,7 +31,7 @@
- + @if (!String.IsNullOrEmpty(Model.Message)) { @@ -49,43 +50,43 @@
@if (Model.State.Type == (int)CustomStateTypes.Unit) { - @Html.Raw("Unit Status"); + @localizer["UnitStatus"] } else if (Model.State.Type == (int)CustomStateTypes.Personnel) { - @Html.Raw("Personnel Actions"); + @localizer["PersonnelActions"] } else if (Model.State.Type == (int)CustomStateTypes.Staffing) { - @Html.Raw("Personnel Staffing"); + @localizer["PersonnelStaffing"] }
- -
+ +
- -
+ +
-
These are the buttons your users will see on mobile applications and the website.
+
@localizer["ButtonsHelp"]
- - - - + + + + @@ -106,8 +107,8 @@ } @@ -121,9 +122,9 @@
- Cancel - - Delete Status Set + @commonLocalizer["Cancel"] + + @commonLocalizer["Delete"]
@@ -138,73 +139,73 @@ diff --git a/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/EditDetail.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/EditDetail.cshtml index 54921891..0b15e02a 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/EditDetail.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/EditDetail.cshtml @@ -1,24 +1,25 @@ @using Resgrid.Model @model Resgrid.Web.Areas.User.Models.CustomStatuses.EditDetailView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Edit Custom State Detail"; + ViewBag.Title = "Resgrid | " + @localizer["EditCustomStateDetailHeader"]; }
-

Edit Custom State Detail

+

@localizer["EditCustomStateDetailHeader"]

@@ -40,25 +41,25 @@
- +
- +
- +
- +
@if (Model.Detail.CustomState.Type != (int)CustomStateTypes.Staffing) {
- +
@Html.CheckBoxFor(m => m.Detail.GpsRequired) @@ -66,7 +67,7 @@
- +
@Html.DropDownListFor(m => m.DetailType, Model.DetailTypes) @@ -75,7 +76,7 @@
}
- +
@Html.DropDownListFor(m => m.NoteType, Model.NoteTypes) @@ -83,7 +84,7 @@
- + @@ -91,8 +92,8 @@
- Cancel - + @commonLocalizer["Cancel"] +
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/Index.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/Index.cshtml index 00896fe8..1fd62f5f 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/Index.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/Index.cshtml @@ -1,20 +1,20 @@ - -@using Resgrid.Model +@using Resgrid.Model @using Resgrid.Web.Helpers @model Resgrid.Web.Areas.User.Models.CustomStatuses.CustomStatusesIndexView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Custom Statuses"; + ViewBag.Title = "Resgrid | " + @localizer["CustomStatusesHeader"]; }
-

Custom Statuses

+

@localizer["CustomStatusesHeader"]

@@ -25,7 +25,7 @@
-
Current Unit Statuses
+
@localizer["CurrentUnitStatusesHeader"]
Add Unit Statuses @@ -38,13 +38,13 @@
@@ -73,8 +73,8 @@ @@ -91,12 +91,12 @@
-
Custom Personnel Statuses (Actions)
+
@localizer["CurrentPersonnelStatusesHeader"]
@@ -104,7 +104,7 @@
@@ -131,12 +131,12 @@
-
Custom Personnel Staffing Levels
+
@localizer["CustomPersonnelStaffingLevelsHeader"]
@@ -144,7 +144,7 @@
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/New.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/New.cshtml index 28954875..cc7b90c2 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/New.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/CustomStatuses/New.cshtml @@ -1,22 +1,23 @@  @using Resgrid.Model @model Resgrid.Web.Areas.User.Models.CustomStatuses.NewCustomStateView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | New Custom Statuses"; + ViewBag.Title = "Resgrid | " + @localizer["NewCustomStatusesHeader"]; }
-

New Custom Statuses

+

@localizer["NewCustomStatusesHeader"]

@@ -44,43 +45,43 @@
@if (Model.Type == CustomStateTypes.Unit) { - @Html.Raw("Unit Status"); + @localizer["UnitStatus"] } else if (Model.Type == CustomStateTypes.Personnel) { - @Html.Raw("Personnel Actions"); + @localizer["PersonnelActions"] } else if (Model.Type == CustomStateTypes.Staffing) { - @Html.Raw("Personnel Staffing"); + @localizer["PersonnelStaffing"] }
- -
+ +
- -
+ +
-
These are the buttons your users will see on mobile applications and the website.
+
@localizer["ButtonsHelp"]
OrderButton TextButton PreviewAdd Option (Button)@commonLocalizer["Order"]@localizer["ButtonText"]@localizer["ButtonPreview"]@localizer["AddOption"]
- Edit - Delete + @commonLocalizer["Edit"] + @commonLocalizer["Delete"]
- Name + @commonLocalizer["Name"] - Count + @commonLocalizer["Count"] - Description + @commonLocalizer["Description"] @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) { - Delete - Edit + @commonLocalizer["Delete"] + @commonLocalizer["Edit"] }
- - - - + + + + @@ -92,8 +93,8 @@
- Cancel - + @commonLocalizer["Cancel"] +
@@ -108,76 +109,76 @@ diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Department/Address.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Department/Address.cshtml index 1f3c9fd8..160cb1c7 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Department/Address.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Department/Address.cshtml @@ -1,17 +1,16 @@ - -@model Resgrid.Web.Areas.User.Models.Settings.SettingsAddressModel +@model Resgrid.Web.Areas.User.Models.Settings.SettingsAddressModel +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Department Address"; + ViewBag.Title = "Resgrid | " + @localizer["DepartmentAddressHeader"]; Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; } -
-

Department Address

+

@localizer["DepartmentAddressHeader"]

@@ -21,7 +20,7 @@ -
Department Address
+
@localizer["DepartmentAddressHeader"]
@using (Html.BeginForm("Address", "Department", FormMethod.Post, new { area = "User", @class = "form-horizontal" })) @@ -47,7 +46,7 @@
@Html.TextBoxFor(m => m.Department.Address.Address1, new { @class = "form-control" }) @@ -55,7 +54,7 @@
@Html.TextBoxFor(m => m.Department.Address.City, new { @class = "form-control" }) @@ -63,7 +62,7 @@
@Html.TextBoxFor(m => m.Department.Address.State, new { @class = "form-control" }) @@ -71,7 +70,7 @@
@Html.TextBoxFor(m => m.Department.Address.PostalCode, new { @class = "form-control" }) @@ -79,15 +78,14 @@
@Html.TextBoxFor(m => m.Department.Address.Country, new { @class = "form-control" })
- +
}
@@ -95,6 +93,6 @@
@section Scripts -{ + { } diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Department/Api.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Department/Api.cshtml index 1ceaa82f..628338b8 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Department/Api.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Department/Api.cshtml @@ -1,17 +1,18 @@ @model Resgrid.Web.Areas.User.Models.DepartmentSettingsModel +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | API Settings"; + ViewBag.Title = "Resgrid | " + @localizer["ApiSettingsHeader"]; }
-

API Settings

+

@localizer["ApiSettingsHeader"]

@@ -23,18 +24,18 @@
-
RSS Active Call Feed
+
@localizer["RssActiveCallFeedHeader"]

- Resgrid allows your department’s active calls to be accessed via a RSS feed. This allows any feed reader (like a blog reader, or even Microsoft Outlook) to actively display and notify of new calls. For maximum compatible Resgrid will generate a unique URL for RSS 2.0 readers. This URL has no other protection (username\password, etc) so it’s important that it’s not publicly shared if you which not to have active calls data public. + @localizer["RssActiveCallFeedInfo"]

@if (String.IsNullOrEmpty(Model.ActiveCallRssKey)) {

- Your department does not have RSS feed Urls provisioned. Click the button below to provision your RSS Urls. Only provision RSS\Atom Urls when your department needs it. + @localizer["RssActiveCallNoFeedInfo"]

- Provision Rss\Atm Urls + @localizer["Provision"]
} else @@ -43,11 +44,9 @@
RSS 2.0
https://api.resgrid.com/api/v3/Feeds/GetActiveCallsAsRSS/@Model.ActiveCallRssKey
- @*
Atom 1.0
-
http://api.resgrid.com/Feeds/GetActiveCallsAsAtom/@Model.ActiveCallRssKey
*@
- Regenerate RSS Urls + @localizer["Regenerate"]
}
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Department/CallSettings.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Department/CallSettings.cshtml index a641901f..95080c3e 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Department/CallSettings.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Department/CallSettings.cshtml @@ -1,265 +1,265 @@ - -@model Resgrid.Web.Areas.User.Models.Departments.CallSettings.CallSettingsView +@model Resgrid.Web.Areas.User.Models.Departments.CallSettings.CallSettingsView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Call Settings"; + ViewBag.Title = "Resgrid | " + @localizer["CallSettingsHeader"]; } @section Styles -{ + { } -
-
-

Call Import Settings

- -
-
+
+
+

@localizer["CallSettingsHeader"]

+ +
+
-
-
-
-
-
-
+
+
+
+
+
+ - @Html.AntiForgeryToken() - @Html.HiddenFor(m => m.EmailSettings.DepartmentCallEmailId) - @Html.HiddenFor(m => m.CallType) - @Html.HiddenFor(m => m.TextCallType) -
- @if (!String.IsNullOrEmpty(Model.Message)) - { -
- @Model.Message -
- } + @Html.AntiForgeryToken() + @Html.HiddenFor(m => m.EmailSettings.DepartmentCallEmailId) + @Html.HiddenFor(m => m.CallType) + @Html.HiddenFor(m => m.TextCallType) +
+ @if (!String.IsNullOrEmpty(Model.Message)) + { +
+ @Model.Message +
+ } -
- -
- @Model.InternalDispatchEmail - This is your ALL-CALL email address, all users in your department (who have call notifications enabled) will receive the call alert -
-
-
- -
-
- - -
- This will auto-close your automatically created calls (i.e. via an email, text or audio import process) -
-
-
- -
- - The value above will keep email imported calls open for that long, then they will be closed. The Prune checkbox also needs to be checked for auto closing to occur. -
-
-
- -
-
-
-
-
+
+ +
+ @Model.InternalDispatchEmail + @localizer["DispatchImportEmailHelp"] +
+
+
+ +
+
+ + +
+ @localizer["PruneCallsHelp"] +
+
+
+ +
+ + @localizer["MinutesToKeepOpenHelp"] +
+
+
+ +
+
+
+
+
-
-
- Cancel - -
-
- -
-
-
-
+
+
+ @commonLocalizer["Cancel"] + +
+
+ +
+
+
+
OrderButton TextButton PreviewAdd Option (Button)@commonLocalizer["Order"]@localizer["ButtonText"]@localizer["ButtonPreview"]@localizer["AddOption"]
- - - - - - - -
Phone Number 
-
- -
-
+ + + + + + + + +
Phone Number 
+
+ +
+
@section Scripts -{ + { - + - + - + - + .k-listview { + border: 0; + padding: 0 0 20px 0; + min-width: 0; + } + } diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Department/DeleteDepartment.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Department/DeleteDepartment.cshtml new file mode 100644 index 00000000..19e3b591 --- /dev/null +++ b/Web/Resgrid.WebCore/Areas/User/Views/Department/DeleteDepartment.cshtml @@ -0,0 +1,90 @@ +@model Resgrid.Web.Areas.User.Models.Departments.DeleteDepartmentView +@inject IStringLocalizer localizer +@{ + ViewBag.Title = "Resgrid | " + @localizer["DeleteDepartmentHeader"]; +} + +
+
+

@localizer["DeleteDepartmentHeader"]

+ +
+
+ +
+
+
+
+
+
+
@localizer["DeleteDepartmentHeader"]
+ @if (Model.CurrentDeleteRequest != null) + { +

+ @localizer["DeleteDepartmentCurrentlyPendingInfo"] +

+ +
+
Requested On
+
@Model.CurrentDeleteRequest.QueuedOn.FormatForDepartment(Model.Department)
+
Requested By
+
@Model.Profile.FullName.AsFirstNameLastName
+
To be Completed After
+
@Model.CurrentDeleteRequest.ToBeCompletedOn.Value.FormatForDepartment(Model.Department)
+
+ +
+ @localizer["CancelDepartmentDeletion"] +
+ } + else + { +

+ @localizer["DeleteDepartmentInfo"] +

+ +
+ + @Html.AntiForgeryToken() +
+
+ @localizer["CheckToDelete"] +
+ + +
+ +
+
+ + +
+
+
+ +
+
+ @commonLocalizer["Cancel"] + +
+
+
+ } +
+
+
+
+
+
+ +@section Scripts +{ + +} diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Department/DispatchSettings.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Department/DispatchSettings.cshtml index 722f7841..59b97522 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Department/DispatchSettings.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Department/DispatchSettings.cshtml @@ -1,17 +1,18 @@ @model Resgrid.Web.Areas.User.Models.Departments.DispatchSettingsView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Dispatch Settings"; + ViewBag.Title = "Resgrid | " + @localizer["DispatchSettingsHeader"]; }
-

Dispatch Settings

+

@localizer["DispatchSettingsHeader"]

@@ -30,13 +31,13 @@ @if (Model.SaveSuccess.GetValueOrDefault()) {
- Successfully saved Dispatch Settings. + @localizer["SavedDispatchSettings"]
}

Group Dispatch Settings

- +
@@ -46,7 +47,7 @@
- +
@@ -56,17 +57,17 @@
- +
@Html.DropDownListFor(m => m.ShiftDispatchStatus, Model.StatusLevels, new { style = "width: 70%" })
- +
@Html.DropDownListFor(m => m.ShiftClearStatus, Model.StatusLevels, new { style = "width: 70%" })
- Cancel - + @commonLocalizer["Cancel"] +
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Department/Invites.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Department/Invites.cshtml index 21ec2dd1..38d037e1 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Department/Invites.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Department/Invites.cshtml @@ -1,132 +1,133 @@ @using Resgrid.Model.Helpers @using Resgrid.Web.Helpers @model Resgrid.Web.Areas.User.Models.InvitesView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Invites"; + ViewBag.Title = "Resgrid | " + @localizer["InvitesHeader"]; }
-
-

Manage Invites

- -
+
+

@localizer["InvitesHeader"]

+ +
-
-
-
-
-
- - - - - - - - - - - @foreach (var i in Model.Invites) - { - - - - - +
+
+
+
+
+
- Email Address - - Sent On - - Completed On - -
- @i.EmailAddress - - @i.SentOn.TimeConverterToString(Model.Department) - - @if (i.CompletedOn.HasValue) - { - @Html.Raw(TimeConverterHelper.TimeConverterToString(i.CompletedOn.Value, Model.Department)) - } - else - { - @Html.Raw("Pending") - } - - @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) - { - @if (!i.CompletedOn.HasValue) - { - - Resend Invite - - } - - Delete Invite - - } -
+ + + + + + + + + + @foreach (var i in Model.Invites) + { + + + + + - - } - + + } + -
+ @localizer["EmailAddress"] + + @localizer["SentOn"] + + @localizer["CompletedOn"] + +
+ @i.EmailAddress + + @i.SentOn.TimeConverterToString(Model.Department) + + @if (i.CompletedOn.HasValue) + { + @Html.Raw(TimeConverterHelper.TimeConverterToString(i.CompletedOn.Value, Model.Department)) + } + else + { + @localizer["Pending"] + } + + @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) + { + @if (!i.CompletedOn.HasValue) + { + + @localizer["ResendInvite"] + + } + + @localizer["DeleteInvite"] + + } +
-
-
-
-
-
-
-
-
-
-
Send Invites
-
-
-
+ +
+
+
+
+
+
+
+
+
+
@localizer["SendInvites"]
+
+
+ - @Html.AntiForgeryToken() -
+ @Html.AntiForgeryToken() +
-
- -
- @Html.TextAreaFor(m => m.EmailAddresses, new { @class = "input-xlarge", rows = "5" }) - Enter the email addresses separated by comma's (i.e. "test1@test.com, test2@test.com") -
-
+
+ +
+ @Html.TextAreaFor(m => m.EmailAddresses, new { @class = "input-xlarge", rows = "5" }) + @localizer["EmailAddressesHelp"] +
+
-
-
- -
-
- -
-
-
-
+
+
+ +
+
+ +
+
+
+
@section Scripts -{ - -} \ No newline at end of file + { + +} diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Department/ProvisionFailed.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Department/ProvisionFailed.cshtml index 3151faf2..6e990286 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Department/ProvisionFailed.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Department/ProvisionFailed.cshtml @@ -1,5 +1,6 @@ -@{ - ViewBag.Title = "Resgrid | Provision Failure"; +@inject IStringLocalizer localizer +@{ + ViewBag.Title = "Resgrid | " + @localizer["ProvisionFailureHeader"]; Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; } @@ -7,8 +8,8 @@

Provision Failure

@@ -18,18 +19,18 @@ -
Provision Failure
+
@localizer["ProvisionFailureHeader"]
- Resgrid was unable provision the number you requested, please retry the operation with another number. If you cannot provision a number, please contact us and let us know. + @localizer["ProvisionFailureBody"]
@@ -42,6 +43,6 @@
@section Scripts -{ + { -} \ No newline at end of file +} diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Department/Settings.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Department/Settings.cshtml index a5bcdd16..1c00a2a7 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Department/Settings.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Department/Settings.cshtml @@ -1,276 +1,349 @@ @using Resgrid.Model @model Resgrid.Web.Areas.User.Models.DepartmentSettingsModel +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Department Settings"; + ViewBag.Title = "Resgrid | " + @localizer["DepartmentSettingsHeader"]; }
-
-

Department Settings

- -
-
+
+

@localizer["DepartmentSettingsHeader"]

+ +
+
-
-
-
-
-
+
+
+
+
+
- @Html.AntiForgeryToken() - @Html.HiddenFor(m => m.Department.Address.AddressId) - @Html.HiddenFor(m => m.Department.DepartmentId) -
+ @Html.AntiForgeryToken() + @Html.HiddenFor(m => m.Department.Address.AddressId) + @Html.HiddenFor(m => m.Department.DepartmentId) +
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
- -
-
- Disable automatically setting personnel's status to Available after an hour -
-
-
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+
+ +
+
+ @localizer["DisableAutoAvailableHelp"] +
+
+
-

- Sorting -

-
- -
-
-
- -
-
-
- -
-
-
+

+ @localizer["Sorting"] +

+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
-

- Address -

-
-
-

This is the address of your department; it could be the address of a single station or your district office. This address is used to help locate your district and area, and is used as a fallback for some operations (i.e. centering the big board).

-
-
- -
- -
- @Html.TextBoxFor(m => m.Department.Address.Address1, new { @class = "form-control" }) -
-
-
- -
- @Html.TextBoxFor(m => m.Department.Address.City, new { @class = "form-control" }) -
-
-
- -
- @Html.TextBoxFor(m => m.Department.Address.State, new { @class = "form-control" }) -
-
-
- -
- @Html.TextBoxFor(m => m.Department.Address.PostalCode, new { @class = "form-control" }) -
-
-
- -
- -
-
-
- -
-
- - Latitude (Decimal Notation: i.e. 39.1517) -
-
- - Longitude (Decimal Notation: i.e. -119.4571) -
-
-
+

+ @localizer["Address"] +

+
+
+

@localizer["AddressHelp"]

+
+
+ +
+ +
+ @Html.TextBoxFor(m => m.Department.Address.Address1, new { @class = "form-control" }) +
+
+
+ +
+ @Html.TextBoxFor(m => m.Department.Address.City, new { @class = "form-control" }) +
+
+
+ +
+ @Html.TextBoxFor(m => m.Department.Address.State, new { @class = "form-control" }) +
+
+
+ +
+ @Html.TextBoxFor(m => m.Department.Address.PostalCode, new { @class = "form-control" }) +
+
+
+ +
+ +
+
+
+ +
+
+ + @localizer["LatitudePlaceholderHelp"] +
+
+ + @localizer["LongitudePlaceholderHelp"] +
+
+
-
-
- Cancel - -
+
+
+ @commonLocalizer["Cancel"] + +
+
+
+
-
-
-
-
-
-
-
-
Personnel Staffing Reset
-
-
-
-
-

If you need to reset all your personnel's staffing level every day, you can configure it here. An example would be resetting staffing to unavailable every day so that for red-flag days you are sure personnel that are marked available happened after the reset.

-
-
-
- -
-
-
- - -
-
-
-
-
- -
- -
-
-
- -
- @Html.DropDownListFor(m => m.ResetStaffingTo, Model.StaffingLevels, new { style = "width: 100%" }) -
-
+
+
+
+
+
@localizer["PersonnelStaffingResetHeader"]
+
+
+
+
+

@localizer["PersonnelStaffingResetInfo"]

+
+
+
+ +
+
+
+ + +
+
+
+
+
+ +
+ +
+
+
+ +
+ @Html.DropDownListFor(m => m.ResetStaffingTo, Model.StaffingLevels, new { style = "width: 100%" }) +
+
-
-
-
-
-
Personnel Status Reset
-
-
-
-
-

If you need to reset all your personnel's statuses every day, you can configure it here. An example would be resetting status to "Standing By" every day so that you know personnel status is reset.

-
-
-
- -
-
-
- - -
+
-
-
-
- -
- -
-
-
- -
- @Html.DropDownListFor(m => m.ResetStatusTo, Model.StatusLevels, new { style = "width: 100%" }) -
-
+
+
+
@localizer["PersonnelStatusResetHeader"]
+
+
+
+
+

@localizer["PersonnelStatusResetInfo"]

+
+
+
+ +
+
+
+ + +
+
+
+
+
+ +
+ +
+
+
+ +
+ @Html.DropDownListFor(m => m.ResetStatusTo, Model.StatusLevels, new { style = "width: 100%" }) +
+
-
-
-
-
-
Force Department Update
-
-
-
-
-

Resgrid caches some department data to increase performance. Some examples of data that is cached is staffing levels, department settings, personnel roles\groups\names. If you have issues where some department data is incorrect you might want to clear your departments cache.

-
-
-
-
- -
+
+
+
+
+
@localizer["SupressStaffingsHeader"]
+
+
+
+
+

@localizer["SupressStaffingsInfo"]

+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
+
+
+
@localizer["ForceDepartmentUpdate"]
+
+
+
+
+

@localizer["ForceDepartmentUpdateInfo"]

+
+
+
+
+ +
+
+
+
+
+
+
@localizer["DeleteDepartmentSettingsHeader"]
+
+
+
+
+

@localizer["DeleteDepartmentSettingsInfo"]

+
+
+ +
+
-
-
-
@section Scripts -{ - + { + } diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/AddArchivedCall.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/AddArchivedCall.cshtml index c2d72e3d..1f6e798d 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/AddArchivedCall.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/AddArchivedCall.cshtml @@ -1,185 +1,261 @@ @model Resgrid.Web.Areas.User.Models.Calls.NewCallView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Add Previous Call"; - Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; + ViewBag.Title = "Resgrid | " + @localizer["AddArchivedCallHeader"]; + Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; } @section Styles -{ + { + } -
-
-

Add Archived Call

- -
-
+
+
+

@localizer["AddArchivedCallHeader"]

+ +
+
+ +
+
+
+
+
+
-
-
-
-
-
- +
+
+ @Html.AntiForgeryToken() + @Html.HiddenFor(m => m.Latitude) + @Html.HiddenFor(m => m.Longitude) + @Html.HiddenFor(m => m.Call.ReportingUserId) +
-
-
- @Html.AntiForgeryToken() - @Html.HiddenFor(m => m.Latitude) - @Html.HiddenFor(m => m.Longitude) -
+ @if (!String.IsNullOrEmpty(Model.Message)) + { +
+ @Model.Message +
+ } +
+
- @if (!String.IsNullOrEmpty(Model.Message)) - { -
- @Model.Message -
- } -
-
- -
- -
-
-
- -
-
-
- -
@Html.DropDownListFor(m => m.CallPriority, Model.CallPriorities, new { @style = "width: 120px;", tabindex = "2" })
-
-
- -
@Html.DropDownListFor(m => m.Call.Type, Model.CallTypes, new { @style = "width: 120px;", tabindex = "3" })
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
- - - Find Address - -
-
-
-
- - - Find w3w Location - -
- This is a what3words address. Learn More. -
-
-
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ +
+
+
+ +
+
+
+ +
@Html.DropDownListFor(m => m.CallPriority, Model.CallPriorities, new { @style = "width: 120px;", tabindex = "2" })
+
+
+ +
@Html.DropDownListFor(m => m.Call.Type, Model.CallTypes, new { @style = "width: 120px;", tabindex = "3" })
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+ + @localizer["W3wHelp"] @localizer["LearnMore"] +
+
+
+
+
+
+ +
+ + + + + + + + + +
@localizer["Name"]@localizer["Note"]@localizer["AddLinkedCall"]
+
+
+
+ + -
-
- -
-
- - - WARNING: This process will update ALL the Resgrid call numbers (i.e. 16-55) based on date. This process is not reversable. -
-
-
-
-
- Cancel - -
-
- -
-
-
-
+
+
+
+
+ +
@Html.DropDownListFor(m => m.CallState, Model.CallStates, new { @style = "width: 120px;" })
+
+
+ +
+
+
+ +
+
+ + + @localizer["RegenerateCallWarning"] +
+
+
+
+
+ @commonLocalizer["Cancel"] + +
+
+ +
+
+
+
+
+ + @section Scripts -{ - @if (Model.CenterCoordinates != null && Model.CenterCoordinates.Latitude.HasValue && Model.CenterCoordinates.Longitude.HasValue) - { - - } + { + + + @if (Model.CenterCoordinates != null && Model.CenterCoordinates.Latitude.HasValue && Model.CenterCoordinates.Longitude.HasValue) + { + + } - -} \ No newline at end of file + +} diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/ArchivedCalls.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/ArchivedCalls.cshtml index 04d7c92c..fe8cb0b2 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/ArchivedCalls.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/ArchivedCalls.cshtml @@ -2,30 +2,31 @@ @using Resgrid.Model @using Resgrid.Web.Helpers @model Resgrid.Web.Areas.User.Models.CallsDashboardModel +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Archived Calls"; + ViewBag.Title = "Resgrid | " + @localizer["ArchivedCallsHeader"]; Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; }
-

Archived Calls

+

@localizer["ArchivedCallsHeader"]

@@ -40,7 +41,7 @@ @if (ClaimsAuthorizationHelper.CanCreateCall()) { }
@@ -69,11 +70,11 @@ diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallData.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallData.cshtml index 089ed319..d01f6517 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallData.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallData.cshtml @@ -4,8 +4,9 @@ @using Resgrid.Model.Helpers @using Resgrid.Web.Helpers @model Resgrid.Web.Areas.User.Models.Calls.ViewCallView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Call Data"; + ViewBag.Title = "Resgrid | " + @commonLocalizer["Documents"]; Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; } @section Styles @@ -15,16 +16,16 @@
-

@Model.Call.Name Data

+

@Model.Call.Name @commonLocalizer["Documents"]

@@ -35,7 +36,7 @@
-
Call Documents
+
@localizer["CallDocumentsHeader"]
@@ -43,19 +44,19 @@ - Name + @commonLocalizer["Name"] - Uploaded By + @commonLocalizer["UploadedBy"] - Uploaded On + @commonLocalizer["UploadedOn"] - Type + @commonLocalizer["Type"] - Size + @commonLocalizer["Size"] @@ -74,7 +75,7 @@ } else { - Unknown + @commonLocalizer["Unknown"] } @if (!String.IsNullOrWhiteSpace(a.UserId)) { @@ -91,7 +92,7 @@ } else { - Unknown + @commonLocalizer["Unknown"] } File @@ -100,14 +101,14 @@ { @a.Size.Value } - Download + @commonLocalizer["Download"] } } else { - No files attached to this Call + @localizer["NoFiles"] } @@ -117,10 +118,10 @@ { - + - + }
@@ -135,7 +136,7 @@
-
Call Notes
+
@localizer["CallNotesHeader"]
@@ -145,9 +146,9 @@
- + - +
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallExport.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallExport.cshtml index 8bc4dc90..49674f5e 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallExport.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallExport.cshtml @@ -3,6 +3,7 @@ @using Resgrid.Web @using Resgrid.Web.Helpers @model Resgrid.Web.Areas.User.Models.Dispatch.CallExportView +@inject IStringLocalizer localizer @{ Layout = null; } @@ -11,7 +12,7 @@ - Resgrid Call Export + @localizer["CallExportHeader"] @@ -53,7 +54,7 @@
-

Call Export

+

@localizer["CallExportHeader"]

@@ -62,18 +63,18 @@ - Id + @localizer["CallIdLabel"] @Model.Call.CallId - Number + @localizer["CallNumberLabel"] @Model.Call.Number @@ -114,54 +115,54 @@ @@ -169,23 +170,23 @@ @if (Model.Call.ClosedOn.HasValue) { } else { } @@ -194,23 +195,23 @@ @if (Model.Call != null) { } else { } @@ -221,13 +222,13 @@
- Dispatched Units + @localizer["DispatchedUnits"]
- Incident Id + @localizer["IncidentIdLabel"] @if (!String.IsNullOrWhiteSpace(Model.Call.IncidentNumber)) { @@ -81,11 +82,11 @@ } else { - None + @commonLocalizer["None"] } - Call Id + @localizer["CallIdentifierLabel"] @if (!String.IsNullOrWhiteSpace(Model.Call.ExternalIdentifier)) { @@ -93,11 +94,11 @@ } else { - None + @commonLocalizer["None"] } - Reference Id + @localizer["ReferenceIdLabel"] @if (!String.IsNullOrWhiteSpace(Model.Call.ReferenceNumber)) { @@ -105,7 +106,7 @@ } else { - None + @commonLocalizer["None"] }
- Call Name + @localizer["NameLabel"] @Model.Call.Name - Call Type + @localizer["TypeLabel"] @if (!String.IsNullOrWhiteSpace(Model.Call.Type)) { @Model.Call.Type } else { - No Type + @localizer["NoType"] } - Priority + @localizer["PrioirtyLabel"] @(((CallPriority)Model.Call.Priority).ToString())
- Logged On + @localizer["LoggedOnLabel"] @Model.Call.LoggedOn.TimeConverterToString(Model.Department) - Logged By + @localizer["LoggedByLabel"] @(await UserHelper.GetFullNameForUser(Model.Call.ReportingUserId)) - State + @localizer["StateLabel"] @if (Model.Call.State == (int)CallStates.Active) { - Active + @commonLocalizer["Active"] } else if (Model.Call.State == (int)CallStates.Cancelled) { - Canceled + @commonLocalizer["Canceled"] } else if (Model.Call.State == (int)CallStates.Closed) { - Closed + @commonLocalizer["Closed"] } else if (Model.Call.State == (int)CallStates.Unfounded) { - Unfounded + @commonLocalizer["Unfounded"] }
- Closed On + @localizer["ClosedOnLabel"] @Model.Call.ClosedOn.Value.TimeConverterToString(Model.Department) - Closed By + @localizer["ClosedByLabel"] @(await UserHelper.GetFullNameForUser(Model.Call.ClosedByUserId)) - Closed On - Not Closed + @localizer["ClosedOnLabel"] + @localizer["NotClosed"] - Closed On - Not Closed + @localizer["ClosedByLabel"] + @localizer["NotClosed"] - Call Address + @localizer["LocationLabel"] @Model.Call.Address - GPS + @localizer["GPSLabel"] @Model.Call.GeoLocationData - Call Address - Not Supplied + @localizer["LocationLabel"] + @localizer["NotSupplied"] - GPS - Not Supplied + @localizer["GPSLabel"] + @localizer["NotSupplied"]
- - - + + + @@ -262,13 +263,13 @@
- Dispatched Personnel + @localizer["DispatchedPersonnel"]
NameStationType@commonLocalizer["Name"]@commonLocalizer["Station"]@commonLocalizer["Type"]
- - - + + + @@ -287,31 +288,31 @@
- Nature + @localizer["CallNatureLabel"]
@Html.Raw(Model.Call.NatureOfCall)
- Notes + @localizer["NotesLabel"]
@Html.Raw(Model.Call.Notes)
- Completed Notes + @localizer["CompletedNotesLabel"]
@Html.Raw(Model.Call.CompletedNotes)
- Notes + @commonLocalizer["Notes"]
IDStationName@commonLocalizer["Id"]@commonLocalizer["Station"]@commonLocalizer["Name"]
- - - + + + @@ -331,14 +332,80 @@
- Unit Events + @localizer["CallsLinkedFromThisHeader"] +
TimestampNameNote@commonLocalizer["Timestamp"]@commonLocalizer["Name"]@commonLocalizer["Note"]
+ + + + + + + + + + @foreach (var linkedCalls in Model.Call.References) + { + + + + + + + + } + +
@localizer["CallNumber"]@commonLocalizer["Name"]@localizer["LoggedOnLabel"]@commonLocalizer["Note"]
+ @linkedCalls.TargetCall.Number + + @linkedCalls.TargetCall.Name + + @linkedCalls.TargetCall.LoggedOn.FormatForDepartment(Model.Department) + + @linkedCalls.Note +
+ @localizer["CallsLinkedToThisHeader"] + + + + + + + + + + + @foreach (var linkedCalls in Model.ChildCalls) + { + + + + + + + + } + +
@localizer["CallNumber"]@commonLocalizer["Name"]@localizer["LoggedOnLabel"]@commonLocalizer["Note"]
+ >@linkedCalls.SourceCall.Number + + @linkedCalls.SourceCall.Name + + @linkedCalls.SourceCall.LoggedOn.FormatForDepartment(Model.Department) + + @linkedCalls.Note +
+
+
+
+
+ @localizer["UnitEvents"] - - - - + + + + @@ -366,14 +433,14 @@
- Personnel Events + @localizer["PersonnelEvents"]
UnitEventTimestampNote@commonLocalizer["Unit"]@commonLocalizer["Event"]@commonLocalizer["Timestamp"]@commonLocalizer["Note"]
- - - - + + + + @@ -401,7 +468,7 @@
- Attached Images + @localizer["AttachedImages"] @foreach (var img in Model.Call.Attachments.Where(x => x.CallAttachmentType == 2)) {
@@ -410,15 +477,15 @@
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallExportEx.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallExportEx.cshtml index 506ef310..f50c1511 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallExportEx.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CallExportEx.cshtml @@ -3,6 +3,7 @@ @using Resgrid.Web @using Resgrid.Web.Helpers @model Resgrid.Web.Areas.User.Models.Dispatch.CallExportView +@inject IStringLocalizer localizer @{ Layout = null; } @@ -11,7 +12,7 @@ - Resgrid Call Export + @localizer["CallExportHeader"] @@ -50,7 +51,7 @@
- +
@@ -58,7 +59,7 @@ Resgrid
-

Call Export

+

@localizer["CallExportHeader"]

@@ -67,15 +68,15 @@
@@ -144,22 +145,22 @@ @if (Model.Call.ClosedOn.HasValue) { } else { } @@ -169,22 +170,22 @@ @if (Model.Call != null) { } else { } @@ -196,13 +197,13 @@
- Dispatched Units + @localizer["DispatchedUnits"]
NameEventTimestampNote@commonLocalizer["Name"]@commonLocalizer["Event"]@commonLocalizer["Timestamp"]@commonLocalizer["Note"]
- Name + @commonLocalizer["Name"] @img.Name - Timestamp + @commonLocalizer["Timestamp"] @img.Timestamp?.TimeConverterToString(Model.Department) - Added By + @localizer["AddedBy"] @(await UserHelper.GetFullNameForUser(img.UserId))
- Id + @localizer["CallIdLabel"] @Model.Call.CallId - Number + @localizer["CallNumberLabel"] @Model.Call.Number - Incident Id + @localizer["IncidentIdLabel"] @if (!String.IsNullOrWhiteSpace(Model.Call.IncidentNumber)) { @@ -83,60 +84,60 @@ } else { - None + @commonLocalizer["None"] }
- Call Name + @localizer["NameLabel"] @Model.Call.Name - Call Type + @localizer["TypeLabel"] @if (!String.IsNullOrWhiteSpace(Model.Call.Type)) { @Model.Call.Type } else { - No Type + @localizer["NoType"] } - Priority + @localizer["PrioirtyLabel"] @(((CallPriority)Model.Call.Priority).ToString())
- Logged On + @localizer["LoggedOnLabel"] @Model.Call.LoggedOn.TimeConverterToString(Model.Department) - Logged By + @localizer["LoggedByLabel"] @(await UserHelper.GetFullNameForUser(Model.Call.ReportingUserId)) - State + @localizer["StateLabel"] @if (Model.Call.State == (int)CallStates.Active) { - Active + @commonLocalizer["Active"] } else if (Model.Call.State == (int)CallStates.Cancelled) { - Canceled + @commonLocalizer["Canceled"] } else if (Model.Call.State == (int)CallStates.Closed) { - Closed + @commonLocalizer["Closed"] } else if (Model.Call.State == (int)CallStates.Unfounded) { - Unfounded + @commonLocalizer["Unfounded"] }
- Closed On + @localizer["ClosedOnLabel"] @Model.Call.ClosedOn.Value.TimeConverterToString(Model.Department) - Closed By + @localizer["ClosedByLabel"] @(await UserHelper.GetFullNameForUser(Model.Call.ClosedByUser.UserId)) - Closed On + @localizer["ClosedOnLabel"] Not Closed - Closed By + @localizer["ClosedByLabel"] - Call Address + @localizer["LocationLabel"] @Model.Call.Address - GPS + @localizer["GPSLabel"] @Model.Call.GeoLocationData - Call Address + @localizer["LocationLabel"] Not Supplied - GPS + @localizer["GPSLabel"] Not Supplied
- - - + + + @@ -237,13 +238,13 @@
- Dispatched Personnel + @localizer["DispatchedPersonnel"]
NameStationType@commonLocalizer["Name"]@commonLocalizer["Station"]@commonLocalizer["Type"]
- - - + + + @@ -262,31 +263,31 @@
- Nature + @localizer["CallNatureLabel"]
@Html.Raw(Model.Call.NatureOfCall)
- Notes + @localizer["NotesLabel"]
@Html.Raw(Model.Call.Notes)
- Completed Notes + @localizer["CompletedNotesLabel"]
@Html.Raw(Model.Call.CompletedNotes)
- Notes + @commonLocalizer["Notes"]
IDStationName@commonLocalizer["Id"]@commonLocalizer["Station"]@commonLocalizer["Name"]
- - - + + + @@ -306,14 +307,80 @@
- Unit Events + @localizer["CallsLinkedFromThisHeader"] +
TimestampNameNote@commonLocalizer["Timestamp"]@commonLocalizer["Name"]@commonLocalizer["Note"]
+ + + + + + + + + + @foreach (var linkedCalls in Model.Call.References) + { + + + + + + + + } + +
@localizer["CallNumber"]@commonLocalizer["Name"]@localizer["LoggedOnLabel"]@commonLocalizer["Note"]
+ @linkedCalls.TargetCall.Number + + @linkedCalls.TargetCall.Name + + @linkedCalls.TargetCall.LoggedOn.FormatForDepartment(Model.Department) + + @linkedCalls.Note +
+ @localizer["CallsLinkedToThisHeader"] + + + + + + + + + + + @foreach (var linkedCalls in Model.ChildCalls) + { + + + + + + + + } + +
@localizer["CallNumber"]@commonLocalizer["Name"]@localizer["LoggedOnLabel"]@commonLocalizer["Note"]
+ @linkedCalls.SourceCall.Number + + @linkedCalls.SourceCall.Name + + @linkedCalls.SourceCall.LoggedOn.FormatForDepartment(Model.Department) + + @linkedCalls.Note +
+
+
+
+
+ @localizer["UnitEvents"] - - - - + + + + @@ -341,14 +408,14 @@
- Personnel Events + @localizer["PersonnelEvents"]
UnitEventTimestampNote@commonLocalizer["Unit"]@commonLocalizer["Event"]@commonLocalizer["Timestamp"]@commonLocalizer["Note"]
- - - - + + + + @@ -379,7 +446,7 @@
-

Driving Directions from @Model.Station.Name

+

@localizer["DrivingDirections"] @Model.Station.Name

@@ -396,7 +463,7 @@ }
- Attached Images + @localizer["AttachedImages"] @foreach (var img in Model.Call.Attachments.Where(x => x.CallAttachmentType == 2)) {
@@ -405,15 +472,15 @@
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CloseCall.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CloseCall.cshtml index ad6d92e1..86e876ef 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CloseCall.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/CloseCall.cshtml @@ -1,7 +1,7 @@ - +@inject IStringLocalizer localizer @model Resgrid.Web.Areas.User.Models.Calls.CloseCallView @{ - ViewBag.Title = "Resgrid | Close Call"; + ViewBag.Title = "Resgrid | " + @localizer["CloseCallHeader"]; Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; } @section Styles @@ -11,16 +11,16 @@
-

Close Call

+

@localizer["CloseCallHeader"]

@@ -42,11 +42,11 @@
- +
@Html.DropDownListFor(m => m.CallState, Model.CallStates, new { @style = "width: 120px;" })
- +
@@ -55,14 +55,14 @@
- -
+ +
- Cancel - + @commonLocalizer["Cancel"] +
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/Dashboard.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/Dashboard.cshtml index 50cad194..baa8d0dc 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/Dashboard.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/Dashboard.cshtml @@ -1,9 +1,9 @@ @model Resgrid.Web.Areas.User.Models.CallsDashboardModel - @using Resgrid.Model @using Resgrid.Web.Helpers +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Dispatch"; + ViewBag.Title = "Resgrid | " + @commonLocalizer["CallsModule"]; Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; } @section Styles @@ -27,23 +27,23 @@
-

Calls

+

@commonLocalizer["CallsModule"]

- Archived Calls + @localizer["ArchivedCalls"] @if (ClaimsAuthorizationHelper.CanCreateCall()) { - New Call + @localizer["NewCall"] }
@@ -55,7 +55,7 @@
-
Unit Statuses
+
@localizer["UnitStatusesHeader"]
@@ -77,12 +77,12 @@
-
Active Calls
+
@localizer["ActiveCallsHeader"]
@if (ClaimsAuthorizationHelper.CanCreateCall()) { }
@@ -103,15 +103,15 @@ diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/DeleteCall.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/DeleteCall.cshtml index bc8c3f77..93f7730e 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/DeleteCall.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/DeleteCall.cshtml @@ -1,7 +1,7 @@ - +@inject IStringLocalizer localizer @model Resgrid.Web.Areas.User.Models.Calls.DeleteCallView @{ - ViewBag.Title = "Resgrid | Delete Call"; + ViewBag.Title = "Resgrid | " + @localizer["DeleteCallHeader"]; Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; } @section Styles @@ -11,16 +11,16 @@
-

Delete Call

+

@localizer["DeleteCallHeader"]

@@ -42,14 +42,14 @@
- -
+ +
- Cancel - + @commonLocalizer["Cancel"] +
diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/FlagCallNote.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/FlagCallNote.cshtml index 6c75f469..32a3de56 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/FlagCallNote.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/FlagCallNote.cshtml @@ -1,21 +1,22 @@ @using Resgrid.Model @model Resgrid.WebCore.Areas.User.Models.Dispatch.FlagCallNoteView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | Flag Call Note"; + ViewBag.Title = "Resgrid | " + @localizer["FlagCallNoteHeader"]; }
-

Flag Call Note

+

@localizer["FlagCallNoteHeader"]

@@ -44,34 +45,34 @@
- +
@Html.Raw(Model.CallNote)
-
- +
+
@Html.Raw(Model.AddedOn)
-
- +
+
@Html.Raw(Model.AddedBy)
- +
- -
+ +
- +
- Cancel - + @commonLocalizer["Cancel"] +
@@ -82,23 +83,23 @@
@section Scripts -{ - + $('#UnitTypeIcon').on('change', function() { + if (this.value >= 0) { + $('#newUnitTypePreview').show(); + let text = $("#UnitTypeIcon option:selected").text().replace(/\s/g, '').toLowerCase(); + $("#newUnitTypePreview").attr("src", "/images/Mapping/" + text + ".png"); + } else { + $('#newUnitTypePreview').hide(); + } + }); + }); + } diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/NewCall.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/NewCall.cshtml index d2720423..6a0114e0 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/NewCall.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Dispatch/NewCall.cshtml @@ -1,6 +1,7 @@ @model Resgrid.Web.Areas.User.Models.Calls.NewCallView +@inject IStringLocalizer localizer @{ - ViewBag.Title = "Resgrid | New Call"; + ViewBag.Title = "Resgrid | " + @localizer["NewCallHeader"]; Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; } @section Styles @@ -18,26 +19,26 @@
-

New Call

+

@localizer["NewCallHeader"]

- Template + @localizer["Template"] @if (!String.IsNullOrWhiteSpace(Model.NewCallFormData)) { - Call Form + @localizer["CallForm"] }
@@ -70,43 +71,43 @@
- -
+ +
- +
@Html.DropDownListFor(m => m.CallPriority, Model.CallPriorities, new { @style = "width: 120px;", tabindex = "2" })
- +
@Html.DropDownListFor(m => m.Call.Type, Model.CallTypes, new { @style = "width: 120px;", tabindex = "3" })
- -
+ +
- -
+ +
- -
+ +
- -
+ +
- -
+ +
- -
+ +
- +
@@ -116,18 +117,18 @@
- -
+ +
- +
@@ -135,24 +136,24 @@ - This is a what3words address. Learn More. + @localizer["W3wHelp"] @localizer["LearnMore"]
- +
NameEventTimestampNote@commonLocalizer["Name"]@commonLocalizer["Event"]@commonLocalizer["Timestamp"]@commonLocalizer["Note"]
- Name + @commonLocalizer["Name"] @img.Name - Timestamp + @commonLocalizer["Timestamp"] @img.Timestamp?.TimeConverterToString(Model.Department) - Added By + @localizer["AddedBy"] @(await UserHelper.GetFullNameForUser(img.UserId))
- - - + + + @@ -160,13 +161,28 @@
- + +
+
CodeNameStatus@commonLocalizer["Code"]@commonLocalizer["Name"]@commonLocalizer["Status"]
+ + + + + + + + +
@localizer["Name"]@localizer["Note"]@localizer["AddLinkedCall"]
+
+
+
+
@@ -184,7 +200,7 @@
- +
-
-
-
-
-
-
- @if (!String.IsNullOrWhiteSpace(Model.Call.GeoLocationData)) - { -
-
-
- Route - - +
+
+
+
+ @if (!String.IsNullOrWhiteSpace(Model.Call.GeoLocationData)) + { +
+
+
+ @commonLocalizer["Route"] + + +
+
+
+ } +
+
+

Notes

+
+
+
+ +
+ + + + + + +
+
+
+
-
- } -
-
-

Notes

-
-
- -
- -
- - - - - - -
-
-
-
-
-
+ View Document @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin() || @Model.Document.UserId == Model.UserId) { Delete diff --git a/Web/Resgrid.WebCore/Areas/User/Views/Home/Dashboard.cshtml b/Web/Resgrid.WebCore/Areas/User/Views/Home/Dashboard.cshtml index 69aac4e3..28e81ae5 100644 --- a/Web/Resgrid.WebCore/Areas/User/Views/Home/Dashboard.cshtml +++ b/Web/Resgrid.WebCore/Areas/User/Views/Home/Dashboard.cshtml @@ -1,9 +1,11 @@ @using Resgrid.Web.Helpers +@using Resgrid.Localization.Areas.User.Home @model Resgrid.Web.Areas.User.Models.DashboardModel @{ ViewBag.Title = "Resgrid | Dashboard"; Layout = "~/Areas/User/Views/Shared/_UserLayout.cshtml"; } +@inject IStringLocalizer localizer @section Styles { @@ -49,14 +51,14 @@ @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) {
  • - Reset all to Available + @localizer["ResetAll"]
  • }
  • - Collapse Groups (Once) + @localizer["CollapseGroups"]
  • - Toggle Collapse Groups (Saves) + @localizer["ToggleCollapseGroupsOnce"]
  • @@ -93,7 +95,7 @@
    @@ -139,7 +141,7 @@
    - +
    @foreach (var state in Model.States.GetActiveDetails()) @@ -155,7 +157,7 @@
    -
    Staffing Level
    +
    @localizer["StaffingLevel"]
    @using (Html.BeginForm("SetUserState", "Home", FormMethod.Post, new { Area = "User", style = "padding-bottom:0px;" })) @@ -177,28 +179,28 @@
    }
    - +
    }
    -
    Department Info
    +
    @localizer["DepartmentInfo"]
    • - Department Id: @ClaimsAuthorizationHelper.GetDepartmentId().ToString() + @localizer["DepartmentId"] @ClaimsAuthorizationHelper.GetDepartmentId().ToString()
    • - Department Code: @Model.Department.Code + @localizer["DepartmentCode"] @Model.Department.Code
    • @if (!String.IsNullOrWhiteSpace(Model.Number)) {
    • - Text Number: @Model.Number + @localizer["TextNumber"] @Model.Number
    • }
    @@ -213,34 +215,15 @@ -
    -
    - -