Skip to content

Commit b712e99

Browse files
authored
Merge pull request #396 from Resgrid/develop
RE1-T121 Adding in better reporting capibility
2 parents 36d15d8 + c2e0bca commit b712e99

68 files changed

Lines changed: 3621 additions & 13 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.ComponentModel;
2+
using System.ComponentModel.DataAnnotations;
3+
4+
namespace Resgrid.Model.Reporting
5+
{
6+
/// <summary>
7+
/// Canonical, cross-department operational availability classification for a person or unit.
8+
/// Produced by <see cref="AvailabilityMatrix"/> from a built-in or custom status' base type.
9+
/// This is the single vocabulary the Dispatch app and reporting use to answer
10+
/// "is this resource available for a call?" regardless of a department's custom status labels.
11+
/// </summary>
12+
public enum AvailabilityClass
13+
{
14+
/// <summary>Status could not be mapped to a known base type.</summary>
15+
[Description("Unknown")]
16+
[Display(Name = "Unknown")]
17+
Unknown = 0,
18+
19+
/// <summary>Available to be assigned/dispatched to a call.</summary>
20+
[Description("Available")]
21+
[Display(Name = "Available")]
22+
Available = 1,
23+
24+
/// <summary>Engaged on a call/assignment and not available for another.</summary>
25+
[Description("Committed")]
26+
[Display(Name = "Committed")]
27+
Committed = 2,
28+
29+
/// <summary>Out of service / not responding / unavailable.</summary>
30+
[Description("Unavailable")]
31+
[Display(Name = "Unavailable")]
32+
Unavailable = 3,
33+
34+
/// <summary>Available but with a delay (e.g. delayed response).</summary>
35+
[Description("Delayed")]
36+
[Display(Name = "Delayed")]
37+
Delayed = 4
38+
}
39+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using System.Collections.Generic;
2+
3+
namespace Resgrid.Model.Reporting
4+
{
5+
/// <summary>
6+
/// Hard-coded, NON per-department mapping of a resource's base status type to a canonical
7+
/// <see cref="AvailabilityClass"/>. This is the single place the system knows what a base status
8+
/// type *means* operationally (e.g. a "Responding" base type means the person/unit is not
9+
/// available for another call).
10+
///
11+
/// Status resolution rules (applied by the reporting service, not by SQL):
12+
/// * Personnel current status lives on the latest <c>ActionLog.ActionTypeId</c>, which holds
13+
/// EITHER a built-in <see cref="ActionTypes"/> value OR a <c>CustomStateDetailId</c>.
14+
/// * Unit current status lives on the latest <c>UnitState.State</c>, which holds EITHER a
15+
/// built-in <see cref="UnitStateTypes"/> value OR a <c>CustomStateDetailId</c>.
16+
/// * A custom status maps to a canonical base via <c>CustomStateDetail.BaseType</c>
17+
/// (an <see cref="ActionBaseTypes"/> value), which then maps here via
18+
/// <see cref="ForPersonnelBaseType"/> / <see cref="ForCustomBaseType"/>.
19+
///
20+
/// NOTE: built-in enum values (0..n) overlap numerically with low CustomStateDetailId values, so
21+
/// disambiguation is done by the service using the department's known set of custom state detail
22+
/// ids (see PlatformReportingService) — NOT by numeric range. The helpers here only translate an
23+
/// already-classified base/built-in value into an <see cref="AvailabilityClass"/>.
24+
/// </summary>
25+
public static class AvailabilityMatrix
26+
{
27+
/// <summary>Highest built-in <see cref="ActionTypes"/> value (personnel).</summary>
28+
public const int MaxBuiltInActionType = (int)ActionTypes.OnUnit; // 7
29+
30+
/// <summary>Highest built-in <see cref="UnitStateTypes"/> value (units).</summary>
31+
public const int MaxBuiltInUnitStateType = (int)UnitStateTypes.Enroute; // 12
32+
33+
// Canonical base type (ActionBaseTypes) -> availability. Used for custom statuses (personnel
34+
// and units both store CustomStateDetail.BaseType as an ActionBaseTypes value).
35+
private static readonly IReadOnlyDictionary<int, AvailabilityClass> ByActionBaseType = new Dictionary<int, AvailabilityClass>
36+
{
37+
{ (int)ActionBaseTypes.None, AvailabilityClass.Unknown },
38+
{ (int)ActionBaseTypes.Available, AvailabilityClass.Available },
39+
{ (int)ActionBaseTypes.NotResponding, AvailabilityClass.Unavailable },
40+
{ (int)ActionBaseTypes.Responding, AvailabilityClass.Committed },
41+
{ (int)ActionBaseTypes.OnScene, AvailabilityClass.Committed },
42+
{ (int)ActionBaseTypes.MadeContact, AvailabilityClass.Committed },
43+
{ (int)ActionBaseTypes.Investigating, AvailabilityClass.Committed },
44+
{ (int)ActionBaseTypes.Dispatched, AvailabilityClass.Committed },
45+
{ (int)ActionBaseTypes.Cleared, AvailabilityClass.Available },
46+
{ (int)ActionBaseTypes.Returning, AvailabilityClass.Committed },
47+
{ (int)ActionBaseTypes.Staging, AvailabilityClass.Committed },
48+
{ (int)ActionBaseTypes.Unavailable, AvailabilityClass.Unavailable },
49+
};
50+
51+
// Built-in personnel status (ActionTypes) -> availability.
52+
private static readonly IReadOnlyDictionary<int, AvailabilityClass> ByActionType = new Dictionary<int, AvailabilityClass>
53+
{
54+
{ (int)ActionTypes.StandingBy, AvailabilityClass.Available }, // "Available"
55+
{ (int)ActionTypes.NotResponding, AvailabilityClass.Unavailable },
56+
{ (int)ActionTypes.Responding, AvailabilityClass.Committed },
57+
{ (int)ActionTypes.OnScene, AvailabilityClass.Committed },
58+
{ (int)ActionTypes.AvailableStation, AvailabilityClass.Available },
59+
{ (int)ActionTypes.RespondingToStation, AvailabilityClass.Committed },
60+
{ (int)ActionTypes.RespondingToScene, AvailabilityClass.Committed },
61+
{ (int)ActionTypes.OnUnit, AvailabilityClass.Available }, // staffing a unit; unit state is authoritative in richer views
62+
};
63+
64+
// Built-in unit status (UnitStateTypes) -> availability.
65+
private static readonly IReadOnlyDictionary<int, AvailabilityClass> ByUnitStateType = new Dictionary<int, AvailabilityClass>
66+
{
67+
{ (int)UnitStateTypes.Available, AvailabilityClass.Available },
68+
{ (int)UnitStateTypes.Delayed, AvailabilityClass.Delayed },
69+
{ (int)UnitStateTypes.Unavailable, AvailabilityClass.Unavailable },
70+
{ (int)UnitStateTypes.Committed, AvailabilityClass.Committed },
71+
{ (int)UnitStateTypes.OutOfService, AvailabilityClass.Unavailable },
72+
{ (int)UnitStateTypes.Responding, AvailabilityClass.Committed },
73+
{ (int)UnitStateTypes.OnScene, AvailabilityClass.Committed },
74+
{ (int)UnitStateTypes.Staging, AvailabilityClass.Committed },
75+
{ (int)UnitStateTypes.Returning, AvailabilityClass.Committed },
76+
{ (int)UnitStateTypes.Cancelled, AvailabilityClass.Available },
77+
{ (int)UnitStateTypes.Released, AvailabilityClass.Available },
78+
{ (int)UnitStateTypes.Manual, AvailabilityClass.Unknown },
79+
{ (int)UnitStateTypes.Enroute, AvailabilityClass.Committed },
80+
};
81+
82+
/// <summary>Maps a canonical personnel base type (<see cref="ActionBaseTypes"/>) to availability.</summary>
83+
public static AvailabilityClass ForPersonnelBaseType(int actionBaseType) =>
84+
ByActionBaseType.TryGetValue(actionBaseType, out var c) ? c : AvailabilityClass.Unknown;
85+
86+
/// <summary>
87+
/// Maps a custom status' <c>CustomStateDetail.BaseType</c> (an <see cref="ActionBaseTypes"/> value)
88+
/// to availability. Applies to both personnel and unit custom statuses.
89+
/// </summary>
90+
public static AvailabilityClass ForCustomBaseType(int actionBaseType) =>
91+
ForPersonnelBaseType(actionBaseType);
92+
93+
/// <summary>Maps a built-in personnel status (<see cref="ActionTypes"/>) to availability.</summary>
94+
public static AvailabilityClass ForBuiltInPersonnelActionType(int actionType) =>
95+
ByActionType.TryGetValue(actionType, out var c) ? c : AvailabilityClass.Unknown;
96+
97+
/// <summary>Maps a built-in unit status (<see cref="UnitStateTypes"/>) to availability.</summary>
98+
public static AvailabilityClass ForUnitStateType(int unitStateType) =>
99+
ByUnitStateType.TryGetValue(unitStateType, out var c) ? c : AvailabilityClass.Unknown;
100+
}
101+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Collections.Generic;
2+
3+
namespace Resgrid.Model.Reporting
4+
{
5+
/// <summary>
6+
/// A grouped (GROUP BY) breakdown, capped to top-N + "Other". Examples keyed as
7+
/// "callsByType", "callsByPriority", "callsByStatus", "personnelByState", "unitsByStatus".
8+
/// </summary>
9+
public class Breakdown
10+
{
11+
public string Key { get; set; }
12+
13+
public List<BreakdownItem> Items { get; set; } = new List<BreakdownItem>();
14+
}
15+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Resgrid.Model.Reporting
2+
{
3+
/// <summary>
4+
/// One slice of a <see cref="Breakdown"/> (e.g. a call type, a priority, a canonical state).
5+
/// Breakdowns are capped to top-N with a synthetic "Other" bucket so payloads stay bounded.
6+
/// </summary>
7+
public class BreakdownItem
8+
{
9+
/// <summary>Resolved, human-readable label for the slice.</summary>
10+
public string Label { get; set; }
11+
12+
/// <summary>The underlying id (type/priority/state id); null for the synthetic "Other" bucket.</summary>
13+
public int? Id { get; set; }
14+
15+
public long Count { get; set; }
16+
17+
/// <summary>True for the synthetic aggregate "Other" bucket.</summary>
18+
public bool IsOther { get; set; }
19+
20+
/// <summary>
21+
/// Canonical availability of this slice; populated only for personnel/unit state breakdowns.
22+
/// </summary>
23+
public AvailabilityClass? Availability { get; set; }
24+
}
25+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace Resgrid.Model.Reporting
5+
{
6+
/// <summary>
7+
/// Composite, read-only dashboard payload returned in a single call. Contains scalar totals,
8+
/// dense (zero-filled, UTC) time series, and bounded top-N+other breakdowns.
9+
///
10+
/// <see cref="DepartmentId"/> is null for a SYSTEM-WIDE (cross-department) report, which is
11+
/// produced only for the in-process BackOffice (Resgrid staff). Department-scoped HTTP callers
12+
/// always receive their own department's data.
13+
///
14+
/// Contains counts only (no PII), which is what makes cross-tenant aggregation safe.
15+
/// </summary>
16+
public class DashboardReport
17+
{
18+
/// <summary>The department this report is scoped to; null = system-wide (BackOffice only).</summary>
19+
public int? DepartmentId { get; set; }
20+
21+
public DateTime StartUtc { get; set; }
22+
public DateTime EndUtc { get; set; }
23+
public ReportGranularity Granularity { get; set; }
24+
25+
/// <summary>When this report was generated (UTC).</summary>
26+
public DateTime GeneratedUtc { get; set; }
27+
28+
/// <summary>
29+
/// IANA/Windows timezone of the requesting context (from the caller's claim), provided so a
30+
/// UI can label/shift the UTC buckets for display. Aggregation itself is always UTC.
31+
/// </summary>
32+
public string TimeZone { get; set; }
33+
34+
public ReportTotals Totals { get; set; } = new ReportTotals();
35+
36+
public List<MetricSeries> Series { get; set; } = new List<MetricSeries>();
37+
38+
public List<Breakdown> Breakdowns { get; set; } = new List<Breakdown>();
39+
}
40+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.ComponentModel;
2+
using System.ComponentModel.DataAnnotations;
3+
4+
namespace Resgrid.Model.Reporting
5+
{
6+
/// <summary>
7+
/// Selects the field set / standardized mapping used when exporting incident or personnel data.
8+
/// </summary>
9+
public enum ExportProfile
10+
{
11+
/// <summary>Generic Resgrid field set (all available columns).</summary>
12+
[Description("Generic")]
13+
[Display(Name = "Generic")]
14+
Generic = 0,
15+
16+
/// <summary>National Fire Incident Reporting System (NFIRS) field mapping (fire).</summary>
17+
[Description("NFIRS")]
18+
[Display(Name = "NFIRS")]
19+
Nfirs = 1,
20+
21+
/// <summary>National EMS Information System (NEMSIS) field mapping (EMS).</summary>
22+
[Description("NEMSIS")]
23+
[Display(Name = "NEMSIS")]
24+
Nemsis = 2
25+
}
26+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
3+
namespace Resgrid.Model.Reporting
4+
{
5+
/// <summary>
6+
/// A single point in a time-bucketed metric series. <see cref="BucketUtc"/> is the start of the
7+
/// day or month (UTC). Series are dense: every bucket in the requested window is present, with
8+
/// <see cref="Value"/> = 0 for buckets that had no data.
9+
/// </summary>
10+
public class MetricPoint
11+
{
12+
public DateTime BucketUtc { get; set; }
13+
14+
public long Value { get; set; }
15+
16+
public MetricPoint() { }
17+
18+
public MetricPoint(DateTime bucketUtc, long value)
19+
{
20+
BucketUtc = bucketUtc;
21+
Value = value;
22+
}
23+
}
24+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Collections.Generic;
2+
3+
namespace Resgrid.Model.Reporting
4+
{
5+
/// <summary>
6+
/// A named, time-bucketed series (e.g. "calls", "messages", "newUsers"). Points are dense
7+
/// (zero-filled) and ordered ascending by bucket.
8+
/// </summary>
9+
public class MetricSeries
10+
{
11+
/// <summary>Stable machine key, e.g. "calls", "messages", "newUsers".</summary>
12+
public string Key { get; set; }
13+
14+
public ReportGranularity Granularity { get; set; }
15+
16+
public List<MetricPoint> Points { get; set; } = new List<MetricPoint>();
17+
}
18+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace Resgrid.Model.Reporting
5+
{
6+
/// <summary>
7+
/// Personnel participation and compliance analytics: per-member call response rate, event/training
8+
/// attendance, and certification expiry status. Especially relevant to volunteer departments.
9+
/// </summary>
10+
public class ParticipationReport
11+
{
12+
public int? DepartmentId { get; set; }
13+
public DateTime StartUtc { get; set; }
14+
public DateTime EndUtc { get; set; }
15+
public DateTime GeneratedUtc { get; set; }
16+
17+
/// <summary>Number of calls in the window used as the denominator for response rates.</summary>
18+
public long CallsInWindow { get; set; }
19+
20+
public List<MemberParticipation> Members { get; set; } = new List<MemberParticipation>();
21+
22+
/// <summary>Count of members with at least one expired certification.</summary>
23+
public int MembersWithExpiredCertifications { get; set; }
24+
}
25+
26+
public class MemberParticipation
27+
{
28+
public string UserId { get; set; }
29+
public string Name { get; set; }
30+
31+
public long CallsResponded { get; set; }
32+
33+
/// <summary>CallsResponded / calls in window (0..1).</summary>
34+
public double ResponseRate { get; set; }
35+
36+
public long EventsAttended { get; set; }
37+
public long TrainingAttended { get; set; }
38+
39+
public int ExpiredCertifications { get; set; }
40+
public int ExpiringCertifications { get; set; }
41+
}
42+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.ComponentModel;
2+
using System.ComponentModel.DataAnnotations;
3+
4+
namespace Resgrid.Model.Reporting
5+
{
6+
/// <summary>
7+
/// Time-bucketing granularity for reporting series. All buckets are anchored in UTC.
8+
/// </summary>
9+
public enum ReportGranularity
10+
{
11+
[Description("Day")]
12+
[Display(Name = "Day")]
13+
Day = 0,
14+
15+
[Description("Month")]
16+
[Display(Name = "Month")]
17+
Month = 1
18+
}
19+
}

0 commit comments

Comments
 (0)