-
-
Notifications
You must be signed in to change notification settings - Fork 82
RIC-T39 More Backend IC work #418
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
27 changes: 27 additions & 0 deletions
27
Core/Resgrid.Model/IncidentCommand/IncidentCommandBundle.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| using System.Collections.Generic; | ||
|
|
||
| namespace Resgrid.Model | ||
| { | ||
| /// <summary> | ||
| /// Shift-start aggregate for offline IC clients: a render-ready snapshot of every ACTIVE incident command in the | ||
| /// caller's department in a single round-trip. Each <see cref="IncidentCommandBoard"/> carries the COMPUTED | ||
| /// accountability / PAR status that the row-based <see cref="IncidentCommandChanges"/> delta cannot, plus the | ||
| /// active ad-hoc resources. The client stores <see cref="ServerTimestampMs"/> and uses it as the <c>since</c> | ||
| /// cursor for subsequent incremental <c>/Sync/Changes</c> pulls. See | ||
| /// docs/architecture/offline-first-architecture.md (§6 / §9.5). | ||
| /// </summary> | ||
| public class IncidentCommandBundle | ||
| { | ||
| /// <summary>Server clock (Unix epoch ms) captured at the start of the read; seeds the next /Sync/Changes cursor.</summary> | ||
| public long ServerTimestampMs { get; set; } | ||
|
|
||
| /// <summary>One render-ready board (incl. accountability / PAR) per active incident command in the department.</summary> | ||
| public List<IncidentCommandBoard> Boards { get; set; } = new List<IncidentCommandBoard>(); | ||
|
|
||
| /// <summary>Active ad-hoc units across the department's active incidents (aggregated by the caller).</summary> | ||
| public List<IncidentAdHocUnit> AdHocUnits { get; set; } = new List<IncidentAdHocUnit>(); | ||
|
|
||
| /// <summary>Active ad-hoc personnel across the department's active incidents (aggregated by the caller).</summary> | ||
| public List<IncidentAdHocPersonnel> AdHocPersonnel { get; set; } = new List<IncidentAdHocPersonnel>(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
|
|
||
| namespace Resgrid.Model | ||
| { | ||
| /// <summary> | ||
| /// Offline shift-start REFERENCE data: the slowly-changing department configuration + roster an IC app needs to | ||
| /// START and RUN an incident in the field (call types, command templates, units, personnel, groups, POIs, | ||
| /// protocols, accountability config, statuses, feature flags). Pulled once at shift start / on manual refresh; | ||
| /// the LIVE per-incident state comes from /Sync/Bundle (active boards) and /Sync/Changes (deltas). See | ||
| /// docs/architecture/offline-first-architecture.md. Personnel is a SAFE PROJECTION (<see cref="ReferencePersonnel"/>) | ||
| /// — never the raw IdentityUser/UserProfile (which carry credentials + verification codes). | ||
| /// </summary> | ||
| public class SyncReferenceData | ||
| { | ||
| /// <summary>Server clock (Unix epoch ms) captured at the start of the read.</summary> | ||
| public long ServerTimestampMs { get; set; } | ||
|
|
||
| public List<CallType> CallTypes { get; set; } = new List<CallType>(); | ||
|
|
||
| public List<DepartmentCallPriority> CallPriorities { get; set; } = new List<DepartmentCallPriority>(); | ||
|
|
||
| /// <summary>Command-definition templates (predefined swimlanes per call type) used to seed a new command.</summary> | ||
| public List<CommandDefinition> CommandTemplates { get; set; } = new List<CommandDefinition>(); | ||
|
|
||
| public List<Unit> Units { get; set; } = new List<Unit>(); | ||
|
|
||
| public List<UnitType> UnitTypes { get; set; } = new List<UnitType>(); | ||
|
|
||
| public List<ReferenceGroup> Groups { get; set; } = new List<ReferenceGroup>(); | ||
|
|
||
| public List<Poi> Pois { get; set; } = new List<Poi>(); | ||
|
|
||
| public List<PoiType> PoiTypes { get; set; } = new List<PoiType>(); | ||
|
|
||
| public List<DispatchProtocol> Protocols { get; set; } = new List<DispatchProtocol>(); | ||
|
|
||
| public List<CheckInTimerConfig> CheckInTimerConfigs { get; set; } = new List<CheckInTimerConfig>(); | ||
|
|
||
| /// <summary>Department-defined personnel custom statuses.</summary> | ||
| public List<CustomState> PersonnelStates { get; set; } = new List<CustomState>(); | ||
|
|
||
| /// <summary>Department-defined unit custom statuses.</summary> | ||
| public List<CustomState> UnitStates { get; set; } = new List<CustomState>(); | ||
|
|
||
| /// <summary>Safe personnel roster projection (no credentials / contact-verification secrets).</summary> | ||
| public List<ReferencePersonnel> Personnel { get; set; } = new List<ReferencePersonnel>(); | ||
|
|
||
| /// <summary>Resolved feature flags for the department (drives addon/feature gating offline).</summary> | ||
| public List<FeatureFlagEvaluation> Features { get; set; } = new List<FeatureFlagEvaluation>(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Safe, minimal personnel projection for offline rosters — mirrors the field exposure of the existing v4 | ||
| /// PersonnelInfoResultData. Deliberately EXCLUDES the IdentityUser nav, password/security fields, and the | ||
| /// UserProfile contact-verification codes + CalendarSyncToken. | ||
| /// </summary> | ||
| public class ReferencePersonnel | ||
| { | ||
| public string UserId { get; set; } | ||
|
|
||
| public string FirstName { get; set; } | ||
|
|
||
| public string LastName { get; set; } | ||
|
|
||
| public string MobilePhone { get; set; } | ||
|
|
||
| /// <summary>Primary group/station membership, if any.</summary> | ||
| public int? GroupId { get; set; } | ||
|
|
||
| public string GroupName { get; set; } | ||
|
|
||
| /// <summary>Current personnel state (UserState.State); 0 when unknown.</summary> | ||
| public int StateId { get; set; } | ||
|
|
||
| public DateTime? StateTimestamp { get; set; } | ||
| } | ||
|
|
||
| /// <summary>Safe, minimal department group/station projection — excludes the member IdentityUser navs.</summary> | ||
| public class ReferenceGroup | ||
| { | ||
| public int GroupId { get; set; } | ||
|
|
||
| public string Name { get; set; } | ||
|
|
||
| public int? Type { get; set; } | ||
|
|
||
| public int? ParentGroupId { get; set; } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace Resgrid.Model.Services | ||
| { | ||
| /// <summary> | ||
| /// Aggregates the offline shift-start REFERENCE data set (department configuration + a safe personnel roster) into | ||
| /// a single payload, so an IC/Unit app can pull everything it needs to start and run an incident in one round-trip. | ||
| /// The live per-incident state is delivered separately by IIncidentCommandService (board bundle + change deltas). | ||
| /// </summary> | ||
| public interface ISyncService | ||
| { | ||
| Task<SyncReferenceData> GetReferenceDataAsync(int departmentId); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Threading.Tasks; | ||
| using Resgrid.Model; | ||
| using Resgrid.Model.Services; | ||
|
|
||
| namespace Resgrid.Services | ||
| { | ||
| /// <summary> | ||
| /// Aggregates the offline shift-start REFERENCE data set (department configuration + a SAFE personnel roster) into | ||
| /// one payload. Read-only. Personnel is projected to <see cref="ReferencePersonnel"/> so no credentials, security | ||
| /// fields, or contact-verification secrets are exposed. The live per-incident state is delivered separately by the | ||
| /// incident-command bundle (/Sync/Bundle) and delta (/Sync/Changes) endpoints. | ||
| /// </summary> | ||
| public class SyncService : ISyncService | ||
| { | ||
| private readonly ICallsService _callsService; | ||
| private readonly ICommandsService _commandsService; | ||
| private readonly IUnitsService _unitsService; | ||
| private readonly IDepartmentGroupsService _departmentGroupsService; | ||
| private readonly IMappingService _mappingService; | ||
| private readonly IProtocolsService _protocolsService; | ||
| private readonly ICheckInTimerService _checkInTimerService; | ||
| private readonly ICustomStateService _customStateService; | ||
| private readonly IUserProfileService _userProfileService; | ||
| private readonly IUserStateService _userStateService; | ||
| private readonly IFeatureToggleService _featureToggleService; | ||
|
|
||
| public SyncService( | ||
| ICallsService callsService, | ||
| ICommandsService commandsService, | ||
| IUnitsService unitsService, | ||
| IDepartmentGroupsService departmentGroupsService, | ||
| IMappingService mappingService, | ||
| IProtocolsService protocolsService, | ||
| ICheckInTimerService checkInTimerService, | ||
| ICustomStateService customStateService, | ||
| IUserProfileService userProfileService, | ||
| IUserStateService userStateService, | ||
| IFeatureToggleService featureToggleService) | ||
| { | ||
| _callsService = callsService; | ||
| _commandsService = commandsService; | ||
| _unitsService = unitsService; | ||
| _departmentGroupsService = departmentGroupsService; | ||
| _mappingService = mappingService; | ||
| _protocolsService = protocolsService; | ||
| _checkInTimerService = checkInTimerService; | ||
| _customStateService = customStateService; | ||
| _userProfileService = userProfileService; | ||
| _userStateService = userStateService; | ||
| _featureToggleService = featureToggleService; | ||
| } | ||
|
|
||
| public async Task<SyncReferenceData> GetReferenceDataAsync(int departmentId) | ||
| { | ||
| var data = new SyncReferenceData { ServerTimestampMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; | ||
|
|
||
| // Configuration / reference entities returned as-is (audited: no secret scalar fields, no IdentityUser navs). | ||
| data.CallTypes = await _callsService.GetCallTypesForDepartmentAsync(departmentId) ?? new List<CallType>(); | ||
| data.CallPriorities = await _callsService.GetCallPrioritiesForDepartmentAsync(departmentId) ?? new List<DepartmentCallPriority>(); | ||
| data.CommandTemplates = await _commandsService.GetAllCommandsForDepartmentAsync(departmentId) ?? new List<CommandDefinition>(); | ||
| data.Units = await _unitsService.GetUnitsForDepartmentAsync(departmentId) ?? new List<Unit>(); | ||
| data.UnitTypes = await _unitsService.GetUnitTypesForDepartmentAsync(departmentId) ?? new List<UnitType>(); | ||
| data.Pois = await _mappingService.GetPOIsForDepartmentAsync(departmentId) ?? new List<Poi>(); | ||
| data.PoiTypes = await _mappingService.GetPOITypesForDepartmentAsync(departmentId) ?? new List<PoiType>(); | ||
| data.Protocols = await _protocolsService.GetAllProtocolsForDepartmentAsync(departmentId) ?? new List<DispatchProtocol>(); | ||
| data.CheckInTimerConfigs = await _checkInTimerService.GetTimerConfigsForDepartmentAsync(departmentId) ?? new List<CheckInTimerConfig>(); | ||
| data.PersonnelStates = await _customStateService.GetAllActiveCustomStatesForDepartmentAsync(departmentId) ?? new List<CustomState>(); | ||
| data.UnitStates = await _customStateService.GetAllActiveUnitStatesForDepartmentAsync(departmentId) ?? new List<CustomState>(); | ||
| data.Features = await _featureToggleService.EvaluateAllForDepartmentAsync(departmentId) ?? new List<FeatureFlagEvaluation>(); | ||
|
|
||
| // Groups: project to a safe shape. The raw DepartmentGroup.Members carry IdentityUser navs we must not leak, | ||
| // and mutating the (possibly cached) entities to strip them would be unsafe. | ||
| var groups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(departmentId) ?? new List<DepartmentGroup>(); | ||
| data.Groups = groups.Select(g => new ReferenceGroup | ||
| { | ||
| GroupId = g.DepartmentGroupId, | ||
| Name = g.Name, | ||
| Type = g.Type, | ||
| ParentGroupId = g.ParentDepartmentGroupId | ||
| }).ToList(); | ||
|
|
||
| data.Personnel = await BuildPersonnelAsync(departmentId, groups); | ||
|
|
||
| return data; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Builds the SAFE personnel roster (name + mobile, primary group, current state) projected from UserProfile + | ||
| /// UserState — never exposing the IdentityUser nav, password/security fields, or the UserProfile contact- | ||
| /// verification codes / CalendarSyncToken. Mirrors the field exposure of the existing v4 PersonnelInfoResultData. | ||
| /// </summary> | ||
| private async Task<List<ReferencePersonnel>> BuildPersonnelAsync(int departmentId, List<DepartmentGroup> groups) | ||
| { | ||
| var personnel = new List<ReferencePersonnel>(); | ||
|
|
||
| var profiles = await _userProfileService.GetAllProfilesForDepartmentAsync(departmentId); | ||
| if (profiles == null || profiles.Count == 0) | ||
| return personnel; | ||
|
|
||
| // First group membership wins as the member's "primary" group. | ||
| var userGroup = new Dictionary<string, ReferenceGroup>(); | ||
| foreach (var g in groups) | ||
| { | ||
| if (g.Members == null) | ||
| continue; | ||
|
|
||
| foreach (var m in g.Members) | ||
| { | ||
| if (!string.IsNullOrWhiteSpace(m.UserId) && !userGroup.ContainsKey(m.UserId)) | ||
| userGroup[m.UserId] = new ReferenceGroup { GroupId = g.DepartmentGroupId, Name = g.Name }; | ||
| } | ||
| } | ||
|
|
||
| var states = await _userStateService.GetStatesForDepartmentAsync(departmentId) ?? new List<UserState>(); | ||
| var stateByUser = states | ||
| .Where(s => !string.IsNullOrWhiteSpace(s.UserId)) | ||
| .GroupBy(s => s.UserId) | ||
| .ToDictionary(grp => grp.Key, grp => grp.OrderByDescending(s => s.Timestamp).First()); | ||
|
|
||
| foreach (var profile in profiles.Values) | ||
| { | ||
| if (profile == null || string.IsNullOrWhiteSpace(profile.UserId)) | ||
| continue; | ||
|
|
||
| var person = new ReferencePersonnel | ||
| { | ||
| UserId = profile.UserId, | ||
| FirstName = profile.FirstName, | ||
| LastName = profile.LastName, | ||
| MobilePhone = profile.MobileNumber | ||
| }; | ||
|
|
||
| if (userGroup.TryGetValue(profile.UserId, out var grp)) | ||
| { | ||
| person.GroupId = grp.GroupId; | ||
| person.GroupName = grp.Name; | ||
| } | ||
|
|
||
| if (stateByUser.TryGetValue(profile.UserId, out var state)) | ||
| { | ||
| person.StateId = state.State; | ||
| person.StateTimestamp = state.Timestamp; | ||
| } | ||
|
|
||
| personnel.Add(person); | ||
| } | ||
|
|
||
| return personnel; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.