Skip to content

Commit af8b2c9

Browse files
authored
Merge pull request #350 from Resgrid/develop
RE1-T115 Fixing dispatch settings issue
2 parents 22d531b + 6af0ea4 commit af8b2c9

19 files changed

Lines changed: 1141 additions & 255 deletions

File tree

Core/Resgrid.Localization/Areas/User/Department/Department.en.resx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,21 @@
312312
<data name="PersonCallReleaseStatusLabel" xml:space="preserve">
313313
<value>Person Call Release Status</value>
314314
</data>
315+
<data name="UnitCallDispatchStatusLabel" xml:space="preserve">
316+
<value>Unit Call Dispatch Status</value>
317+
</data>
318+
<data name="UnitCallReleaseStatusLabel" xml:space="preserve">
319+
<value>Unit Call Release Status</value>
320+
</data>
321+
<data name="UnitDefaultStatusesHelp" xml:space="preserve">
322+
<value>These default unit statuses only use the built-in unit state types. Use the unit type override table below when a unit type needs one of its own custom statuses.</value>
323+
</data>
324+
<data name="UnitTypeStatusOverridesHeader" xml:space="preserve">
325+
<value>Unit Type Status Overrides</value>
326+
</data>
327+
<data name="UnitTypeStatusOverridesHelp" xml:space="preserve">
328+
<value>Only unit types with custom unit statuses appear here. Leave a value on Default to use the department-wide built-in status above.</value>
329+
</data>
315330
<data name="PersonnelSorting" xml:space="preserve">
316331
<value>Personnel Sorting</value>
317332
</data>

Core/Resgrid.Localization/Areas/User/Department/Department.resx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,21 @@
192192
<data name="PersonnelOnUnitSetUnitStatusHelp" xml:space="preserve">
193193
<value />
194194
</data>
195+
<data name="UnitCallDispatchStatusLabel" xml:space="preserve">
196+
<value>Unit Call Dispatch Status</value>
197+
</data>
198+
<data name="UnitCallReleaseStatusLabel" xml:space="preserve">
199+
<value>Unit Call Release Status</value>
200+
</data>
201+
<data name="UnitDefaultStatusesHelp" xml:space="preserve">
202+
<value>These default unit statuses only use the built-in unit state types. Use the unit type override table below when a unit type needs one of its own custom statuses.</value>
203+
</data>
204+
<data name="UnitTypeStatusOverridesHeader" xml:space="preserve">
205+
<value>Unit Type Status Overrides</value>
206+
</data>
207+
<data name="UnitTypeStatusOverridesHelp" xml:space="preserve">
208+
<value>Only unit types with custom unit statuses appear here. Leave a value on Default to use the department-wide built-in status above.</value>
209+
</data>
195210
<data name="CallDispatchSettingsHeader" xml:space="preserve">
196211
<value />
197212
</data>

Core/Resgrid.Model/DepartmentSettingTypes.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,8 @@ public enum DepartmentSettingTypes
5151
MappingMapboxStyleUrl = 47,
5252
MappingMapboxAccessToken = 48,
5353
TtsLanguage = 49,
54+
UnitCallDispatchStatusToSet = 50,
55+
UnitCallReleaseStatusToSet = 51,
56+
UnitCallStatusOverridesByUnitType = 52,
5457
}
5558
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Collections.Generic;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Resgrid.Model;
5+
6+
namespace Resgrid.Model.Services
7+
{
8+
public interface ICallDispatchStatusService
9+
{
10+
Task ApplyDispatchStatusesAsync(Call call, IEnumerable<int> groupIds = null, IEnumerable<int> unitIds = null, CancellationToken cancellationToken = default(CancellationToken));
11+
12+
Task ApplyReleaseStatusesAsync(Call call, IEnumerable<int> groupIds = null, IEnumerable<int> unitIds = null, CancellationToken cancellationToken = default(CancellationToken));
13+
}
14+
}

Core/Resgrid.Model/Services/IDepartmentSettingsService.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,15 @@ public interface IDepartmentSettingsService
283283

284284
Task<bool> GetUnitDispatchAlsoDispatchToGroupAsync(int departmentId);
285285

286+
Task<int> GetUnitCallDispatchStatusToSetAsync(int departmentId);
287+
288+
Task<int> GetUnitCallReleaseStatusToSetAsync(int departmentId);
289+
290+
Task<List<UnitTypeCallStatusOverride>> GetUnitCallStatusOverridesByUnitTypeAsync(int departmentId);
291+
292+
Task<DepartmentSetting> SetUnitCallStatusOverridesByUnitTypeAsync(int departmentId,
293+
List<UnitTypeCallStatusOverride> overrides, CancellationToken cancellationToken = default(CancellationToken));
294+
286295
Task<bool> GetPersonnelOnUnitSetUnitStatusAsync(int departmentId, bool bypassCache = false);
287296

288297
Task<DepartmentSetting> SetDepartmentModuleSettingsAsync(int departmentId, DepartmentModuleSettings settings, CancellationToken cancellationToken = default(CancellationToken));
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Collections.Generic;
2+
using ProtoBuf;
3+
4+
namespace Resgrid.Model
5+
{
6+
[ProtoContract]
7+
public class UnitTypeCallStatusOverride
8+
{
9+
public UnitTypeCallStatusOverride()
10+
{
11+
DispatchStatus = -1;
12+
ReleaseStatus = -1;
13+
}
14+
15+
[ProtoMember(1)]
16+
public int UnitTypeId { get; set; }
17+
18+
[ProtoMember(2)]
19+
public int DispatchStatus { get; set; }
20+
21+
[ProtoMember(3)]
22+
public int ReleaseStatus { get; set; }
23+
}
24+
25+
[ProtoContract]
26+
public class UnitTypeCallStatusOverrideSetting
27+
{
28+
public UnitTypeCallStatusOverrideSetting()
29+
{
30+
Overrides = new List<UnitTypeCallStatusOverride>();
31+
}
32+
33+
[ProtoMember(1)]
34+
public List<UnitTypeCallStatusOverride> Overrides { get; set; }
35+
}
36+
}
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Resgrid.Framework;
7+
using Resgrid.Model;
8+
using Resgrid.Model.Helpers;
9+
using Resgrid.Model.Services;
10+
11+
namespace Resgrid.Services
12+
{
13+
public class CallDispatchStatusService : ICallDispatchStatusService
14+
{
15+
private readonly IDepartmentSettingsService _departmentSettingsService;
16+
private readonly IDepartmentsService _departmentsService;
17+
private readonly IShiftsService _shiftsService;
18+
private readonly IActionLogsService _actionLogsService;
19+
private readonly IUnitsService _unitsService;
20+
private readonly ICustomStateService _customStateService;
21+
22+
public CallDispatchStatusService(
23+
IDepartmentSettingsService departmentSettingsService,
24+
IDepartmentsService departmentsService,
25+
IShiftsService shiftsService,
26+
IActionLogsService actionLogsService,
27+
IUnitsService unitsService,
28+
ICustomStateService customStateService)
29+
{
30+
_departmentSettingsService = departmentSettingsService;
31+
_departmentsService = departmentsService;
32+
_shiftsService = shiftsService;
33+
_actionLogsService = actionLogsService;
34+
_unitsService = unitsService;
35+
_customStateService = customStateService;
36+
}
37+
38+
public async Task ApplyDispatchStatusesAsync(Call call, IEnumerable<int> groupIds = null, IEnumerable<int> unitIds = null, CancellationToken cancellationToken = default(CancellationToken))
39+
{
40+
await ApplyStatusesAsync(call, groupIds, unitIds, true, cancellationToken);
41+
}
42+
43+
public async Task ApplyReleaseStatusesAsync(Call call, IEnumerable<int> groupIds = null, IEnumerable<int> unitIds = null, CancellationToken cancellationToken = default(CancellationToken))
44+
{
45+
await ApplyStatusesAsync(call, groupIds, unitIds, false, cancellationToken);
46+
}
47+
48+
private async Task ApplyStatusesAsync(Call call, IEnumerable<int> groupIds, IEnumerable<int> unitIds, bool isDispatch, CancellationToken cancellationToken)
49+
{
50+
if (call == null)
51+
throw new ArgumentNullException(nameof(call));
52+
53+
var resolvedGroupIds = GetDistinctIds(groupIds, call.GroupDispatches?.Select(x => x.DepartmentGroupId));
54+
var resolvedUnitIds = GetDistinctIds(unitIds, call.UnitDispatches?.Select(x => x.UnitId));
55+
56+
if (!resolvedGroupIds.Any() && !resolvedUnitIds.Any())
57+
return;
58+
59+
var department = await _departmentsService.GetDepartmentByIdAsync(call.DepartmentId);
60+
61+
if (resolvedGroupIds.Any())
62+
await ApplyPersonnelStatusesAsync(call, department, resolvedGroupIds, isDispatch, cancellationToken);
63+
64+
if (resolvedUnitIds.Any())
65+
await ApplyUnitStatusesAsync(call, department, resolvedUnitIds, isDispatch, cancellationToken);
66+
}
67+
68+
private async Task ApplyPersonnelStatusesAsync(Call call, Department department, IReadOnlyCollection<int> groupIds, bool isDispatch, CancellationToken cancellationToken)
69+
{
70+
var dispatchShiftInsteadOfGroup = await _departmentSettingsService.GetDispatchShiftInsteadOfGroupAsync(call.DepartmentId);
71+
var autoSetStatusForShiftPersonnel = await _departmentSettingsService.GetAutoSetStatusForShiftDispatchPersonnelAsync(call.DepartmentId);
72+
73+
if (!dispatchShiftInsteadOfGroup || !autoSetStatusForShiftPersonnel)
74+
return;
75+
76+
var shiftUserIds = await GetShiftUserIdsAsync(call, department, groupIds);
77+
if (!shiftUserIds.Any())
78+
return;
79+
80+
var statusToSet = isDispatch
81+
? await _departmentSettingsService.GetShiftCallDispatchPersonnelStatusToSetAsync(call.DepartmentId)
82+
: await _departmentSettingsService.GetShiftCallReleasePersonnelStatusToSetAsync(call.DepartmentId);
83+
84+
if (statusToSet < 0)
85+
statusToSet = isDispatch ? (int)ActionTypes.RespondingToScene : (int)ActionTypes.StandingBy;
86+
87+
foreach (var userId in shiftUserIds)
88+
{
89+
await _actionLogsService.SetUserActionAsync(userId, call.DepartmentId, statusToSet, null, call.CallId, cancellationToken);
90+
}
91+
}
92+
93+
private async Task ApplyUnitStatusesAsync(Call call, Department department, IReadOnlyCollection<int> unitIds, bool isDispatch, CancellationToken cancellationToken)
94+
{
95+
var defaultStatusToSet = isDispatch
96+
? await _departmentSettingsService.GetUnitCallDispatchStatusToSetAsync(call.DepartmentId)
97+
: await _departmentSettingsService.GetUnitCallReleaseStatusToSetAsync(call.DepartmentId);
98+
99+
if (defaultStatusToSet < 0)
100+
defaultStatusToSet = isDispatch ? (int)UnitStateTypes.Responding : (int)UnitStateTypes.Released;
101+
102+
var resolvedStatuses = await ResolveUnitStatusesAsync(call.DepartmentId, unitIds, defaultStatusToSet, isDispatch);
103+
104+
var timestamp = DateTime.UtcNow;
105+
var localTimestamp = department != null ? DateTimeHelpers.GetLocalDateTime(timestamp, department.TimeZone) : timestamp;
106+
107+
foreach (var unitId in unitIds)
108+
{
109+
var statusToSet = resolvedStatuses.TryGetValue(unitId, out var resolvedStatus) ? resolvedStatus : defaultStatusToSet;
110+
111+
var state = new UnitState
112+
{
113+
UnitId = unitId,
114+
State = statusToSet,
115+
Timestamp = timestamp,
116+
LocalTimestamp = localTimestamp,
117+
DestinationId = call.CallId,
118+
DestinationType = (int)DestinationEntityTypes.Call
119+
};
120+
121+
await _unitsService.SetUnitStateAsync(state, call.DepartmentId, cancellationToken);
122+
}
123+
}
124+
125+
private async Task<Dictionary<int, int>> ResolveUnitStatusesAsync(int departmentId, IReadOnlyCollection<int> unitIds,
126+
int defaultStatusToSet, bool isDispatch)
127+
{
128+
var resolvedStatuses = unitIds.ToDictionary(x => x, _ => defaultStatusToSet);
129+
var unitTypeOverrides = await _departmentSettingsService.GetUnitCallStatusOverridesByUnitTypeAsync(departmentId);
130+
131+
if (unitTypeOverrides == null || !unitTypeOverrides.Any())
132+
return resolvedStatuses;
133+
134+
var unitTypeOverrideLookup = unitTypeOverrides
135+
.Where(x => x != null && x.UnitTypeId > 0)
136+
.GroupBy(x => x.UnitTypeId)
137+
.ToDictionary(x => x.Key, x => x.Last());
138+
139+
if (!unitTypeOverrideLookup.Any())
140+
return resolvedStatuses;
141+
142+
var units = (await Task.WhenAll(unitIds.Select(x => _unitsService.GetUnitByIdAsync(x))))
143+
.Where(x => x != null)
144+
.ToList();
145+
146+
if (!units.Any())
147+
return resolvedStatuses;
148+
149+
var unitTypesByName = new Dictionary<string, UnitType>(StringComparer.OrdinalIgnoreCase);
150+
151+
foreach (var unitTypeName in units
152+
.Select(x => x.Type)
153+
.Where(x => !String.IsNullOrWhiteSpace(x))
154+
.Distinct(StringComparer.OrdinalIgnoreCase))
155+
{
156+
var unitType = await _unitsService.GetUnitTypeByNameAsync(departmentId, unitTypeName);
157+
158+
if (unitType != null)
159+
unitTypesByName[unitTypeName] = unitType;
160+
}
161+
162+
var customStateDetailIdsByStateId = new Dictionary<int, HashSet<int>>();
163+
var customStateIds = unitTypesByName.Values
164+
.Where(x => x.CustomStatesId.HasValue && x.CustomStatesId.Value > 0 && unitTypeOverrideLookup.ContainsKey(x.UnitTypeId))
165+
.Select(x => x.CustomStatesId.Value)
166+
.Distinct()
167+
.ToList();
168+
169+
foreach (var customStateId in customStateIds)
170+
{
171+
var customState = await _customStateService.GetCustomSateByIdAsync(customStateId);
172+
customStateDetailIdsByStateId[customStateId] = customState != null && !customState.IsDeleted
173+
? new HashSet<int>(customState.GetActiveDetails().Select(x => x.CustomStateDetailId))
174+
: new HashSet<int>();
175+
}
176+
177+
foreach (var unit in units)
178+
{
179+
if (String.IsNullOrWhiteSpace(unit.Type))
180+
continue;
181+
182+
if (!unitTypesByName.TryGetValue(unit.Type, out var unitType))
183+
continue;
184+
185+
if (!unitTypeOverrideLookup.TryGetValue(unitType.UnitTypeId, out var unitTypeOverride))
186+
continue;
187+
188+
var candidateStatus = isDispatch ? unitTypeOverride.DispatchStatus : unitTypeOverride.ReleaseStatus;
189+
190+
if (candidateStatus < 0 || !unitType.CustomStatesId.HasValue || unitType.CustomStatesId.Value <= 0)
191+
continue;
192+
193+
if (customStateDetailIdsByStateId.TryGetValue(unitType.CustomStatesId.Value, out var validStateIds) &&
194+
validStateIds.Contains(candidateStatus))
195+
resolvedStatuses[unit.UnitId] = candidateStatus;
196+
}
197+
198+
return resolvedStatuses;
199+
}
200+
201+
private async Task<HashSet<string>> GetShiftUserIdsAsync(Call call, Department department, IReadOnlyCollection<int> groupIds)
202+
{
203+
var shiftUserIds = new HashSet<string>();
204+
var shiftDate = GetShiftDate(call, department);
205+
206+
foreach (var groupId in groupIds)
207+
{
208+
var signups = await _shiftsService.GetShiftSignupsByDepartmentGroupIdAndDayAsync(groupId, shiftDate);
209+
210+
if (signups == null)
211+
continue;
212+
213+
foreach (var signup in signups)
214+
{
215+
if (!String.IsNullOrWhiteSpace(signup.UserId))
216+
shiftUserIds.Add(signup.UserId);
217+
}
218+
}
219+
220+
return shiftUserIds;
221+
}
222+
223+
private static List<int> GetDistinctIds(IEnumerable<int> primaryIds, IEnumerable<int> fallbackIds)
224+
{
225+
return (primaryIds ?? fallbackIds ?? Enumerable.Empty<int>()).Distinct().ToList();
226+
}
227+
228+
private static DateTime GetShiftDate(Call call, Department department)
229+
{
230+
var referenceDate = GetReferenceDate(call);
231+
var localizedDate = department != null ? TimeConverterHelper.TimeConverter(referenceDate, department) : referenceDate;
232+
233+
return new DateTime(localizedDate.Year, localizedDate.Month, localizedDate.Day);
234+
}
235+
236+
private static DateTime GetReferenceDate(Call call)
237+
{
238+
if (call.LastDispatchedOn.HasValue)
239+
return call.LastDispatchedOn.Value;
240+
241+
if (call.DispatchOn.HasValue)
242+
return call.DispatchOn.Value;
243+
244+
if (call.LoggedOn != default(DateTime))
245+
return call.LoggedOn;
246+
247+
return DateTime.UtcNow;
248+
}
249+
}
250+
}

0 commit comments

Comments
 (0)