Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Core/Resgrid.Model/Reporting/AvailabilityClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Resgrid.Model.Reporting
{
/// <summary>
/// Canonical, cross-department operational availability classification for a person or unit.
/// Produced by <see cref="AvailabilityMatrix"/> from a built-in or custom status' base type.
/// This is the single vocabulary the Dispatch app and reporting use to answer
/// "is this resource available for a call?" regardless of a department's custom status labels.
/// </summary>
public enum AvailabilityClass
{
/// <summary>Status could not be mapped to a known base type.</summary>
[Description("Unknown")]
[Display(Name = "Unknown")]
Unknown = 0,

/// <summary>Available to be assigned/dispatched to a call.</summary>
[Description("Available")]
[Display(Name = "Available")]
Available = 1,

/// <summary>Engaged on a call/assignment and not available for another.</summary>
[Description("Committed")]
[Display(Name = "Committed")]
Committed = 2,

/// <summary>Out of service / not responding / unavailable.</summary>
[Description("Unavailable")]
[Display(Name = "Unavailable")]
Unavailable = 3,

/// <summary>Available but with a delay (e.g. delayed response).</summary>
[Description("Delayed")]
[Display(Name = "Delayed")]
Delayed = 4
}
}
101 changes: 101 additions & 0 deletions Core/Resgrid.Model/Reporting/AvailabilityMatrix.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.Collections.Generic;

namespace Resgrid.Model.Reporting
{
/// <summary>
/// Hard-coded, NON per-department mapping of a resource's base status type to a canonical
/// <see cref="AvailabilityClass"/>. This is the single place the system knows what a base status
/// type *means* operationally (e.g. a "Responding" base type means the person/unit is not
/// available for another call).
///
/// Status resolution rules (applied by the reporting service, not by SQL):
/// * Personnel current status lives on the latest <c>ActionLog.ActionTypeId</c>, which holds
/// EITHER a built-in <see cref="ActionTypes"/> value OR a <c>CustomStateDetailId</c>.
/// * Unit current status lives on the latest <c>UnitState.State</c>, which holds EITHER a
/// built-in <see cref="UnitStateTypes"/> value OR a <c>CustomStateDetailId</c>.
/// * A custom status maps to a canonical base via <c>CustomStateDetail.BaseType</c>
/// (an <see cref="ActionBaseTypes"/> value), which then maps here via
/// <see cref="ForPersonnelBaseType"/> / <see cref="ForCustomBaseType"/>.
///
/// NOTE: built-in enum values (0..n) overlap numerically with low CustomStateDetailId values, so
/// disambiguation is done by the service using the department's known set of custom state detail
/// ids (see PlatformReportingService) — NOT by numeric range. The helpers here only translate an
/// already-classified base/built-in value into an <see cref="AvailabilityClass"/>.
/// </summary>
public static class AvailabilityMatrix
{
/// <summary>Highest built-in <see cref="ActionTypes"/> value (personnel).</summary>
public const int MaxBuiltInActionType = (int)ActionTypes.OnUnit; // 7

/// <summary>Highest built-in <see cref="UnitStateTypes"/> value (units).</summary>
public const int MaxBuiltInUnitStateType = (int)UnitStateTypes.Enroute; // 12

// Canonical base type (ActionBaseTypes) -> availability. Used for custom statuses (personnel
// and units both store CustomStateDetail.BaseType as an ActionBaseTypes value).
private static readonly IReadOnlyDictionary<int, AvailabilityClass> ByActionBaseType = new Dictionary<int, AvailabilityClass>
{
{ (int)ActionBaseTypes.None, AvailabilityClass.Unknown },
{ (int)ActionBaseTypes.Available, AvailabilityClass.Available },
{ (int)ActionBaseTypes.NotResponding, AvailabilityClass.Unavailable },
{ (int)ActionBaseTypes.Responding, AvailabilityClass.Committed },
{ (int)ActionBaseTypes.OnScene, AvailabilityClass.Committed },
{ (int)ActionBaseTypes.MadeContact, AvailabilityClass.Committed },
{ (int)ActionBaseTypes.Investigating, AvailabilityClass.Committed },
{ (int)ActionBaseTypes.Dispatched, AvailabilityClass.Committed },
{ (int)ActionBaseTypes.Cleared, AvailabilityClass.Available },
{ (int)ActionBaseTypes.Returning, AvailabilityClass.Committed },
{ (int)ActionBaseTypes.Staging, AvailabilityClass.Committed },
{ (int)ActionBaseTypes.Unavailable, AvailabilityClass.Unavailable },
};

// Built-in personnel status (ActionTypes) -> availability.
private static readonly IReadOnlyDictionary<int, AvailabilityClass> ByActionType = new Dictionary<int, AvailabilityClass>
{
{ (int)ActionTypes.StandingBy, AvailabilityClass.Available }, // "Available"
{ (int)ActionTypes.NotResponding, AvailabilityClass.Unavailable },
{ (int)ActionTypes.Responding, AvailabilityClass.Committed },
{ (int)ActionTypes.OnScene, AvailabilityClass.Committed },
{ (int)ActionTypes.AvailableStation, AvailabilityClass.Available },
{ (int)ActionTypes.RespondingToStation, AvailabilityClass.Committed },
{ (int)ActionTypes.RespondingToScene, AvailabilityClass.Committed },
{ (int)ActionTypes.OnUnit, AvailabilityClass.Available }, // staffing a unit; unit state is authoritative in richer views
};

// Built-in unit status (UnitStateTypes) -> availability.
private static readonly IReadOnlyDictionary<int, AvailabilityClass> ByUnitStateType = new Dictionary<int, AvailabilityClass>
{
{ (int)UnitStateTypes.Available, AvailabilityClass.Available },
{ (int)UnitStateTypes.Delayed, AvailabilityClass.Delayed },
{ (int)UnitStateTypes.Unavailable, AvailabilityClass.Unavailable },
{ (int)UnitStateTypes.Committed, AvailabilityClass.Committed },
{ (int)UnitStateTypes.OutOfService, AvailabilityClass.Unavailable },
{ (int)UnitStateTypes.Responding, AvailabilityClass.Committed },
{ (int)UnitStateTypes.OnScene, AvailabilityClass.Committed },
{ (int)UnitStateTypes.Staging, AvailabilityClass.Committed },
{ (int)UnitStateTypes.Returning, AvailabilityClass.Committed },
{ (int)UnitStateTypes.Cancelled, AvailabilityClass.Available },
{ (int)UnitStateTypes.Released, AvailabilityClass.Available },
{ (int)UnitStateTypes.Manual, AvailabilityClass.Unknown },
{ (int)UnitStateTypes.Enroute, AvailabilityClass.Committed },
};

/// <summary>Maps a canonical personnel base type (<see cref="ActionBaseTypes"/>) to availability.</summary>
public static AvailabilityClass ForPersonnelBaseType(int actionBaseType) =>
ByActionBaseType.TryGetValue(actionBaseType, out var c) ? c : AvailabilityClass.Unknown;

/// <summary>
/// Maps a custom status' <c>CustomStateDetail.BaseType</c> (an <see cref="ActionBaseTypes"/> value)
/// to availability. Applies to both personnel and unit custom statuses.
/// </summary>
public static AvailabilityClass ForCustomBaseType(int actionBaseType) =>
ForPersonnelBaseType(actionBaseType);

/// <summary>Maps a built-in personnel status (<see cref="ActionTypes"/>) to availability.</summary>
public static AvailabilityClass ForBuiltInPersonnelActionType(int actionType) =>
ByActionType.TryGetValue(actionType, out var c) ? c : AvailabilityClass.Unknown;

/// <summary>Maps a built-in unit status (<see cref="UnitStateTypes"/>) to availability.</summary>
public static AvailabilityClass ForUnitStateType(int unitStateType) =>
ByUnitStateType.TryGetValue(unitStateType, out var c) ? c : AvailabilityClass.Unknown;
}
}
15 changes: 15 additions & 0 deletions Core/Resgrid.Model/Reporting/Breakdown.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Collections.Generic;

namespace Resgrid.Model.Reporting
{
/// <summary>
/// A grouped (GROUP BY) breakdown, capped to top-N + "Other". Examples keyed as
/// "callsByType", "callsByPriority", "callsByStatus", "personnelByState", "unitsByStatus".
/// </summary>
public class Breakdown
{
public string Key { get; set; }

public List<BreakdownItem> Items { get; set; } = new List<BreakdownItem>();
}
}
25 changes: 25 additions & 0 deletions Core/Resgrid.Model/Reporting/BreakdownItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Resgrid.Model.Reporting
{
/// <summary>
/// One slice of a <see cref="Breakdown"/> (e.g. a call type, a priority, a canonical state).
/// Breakdowns are capped to top-N with a synthetic "Other" bucket so payloads stay bounded.
/// </summary>
public class BreakdownItem
{
/// <summary>Resolved, human-readable label for the slice.</summary>
public string Label { get; set; }

/// <summary>The underlying id (type/priority/state id); null for the synthetic "Other" bucket.</summary>
public int? Id { get; set; }

public long Count { get; set; }

/// <summary>True for the synthetic aggregate "Other" bucket.</summary>
public bool IsOther { get; set; }

/// <summary>
/// Canonical availability of this slice; populated only for personnel/unit state breakdowns.
/// </summary>
public AvailabilityClass? Availability { get; set; }
}
}
40 changes: 40 additions & 0 deletions Core/Resgrid.Model/Reporting/DashboardReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;

namespace Resgrid.Model.Reporting
{
/// <summary>
/// Composite, read-only dashboard payload returned in a single call. Contains scalar totals,
/// dense (zero-filled, UTC) time series, and bounded top-N+other breakdowns.
///
/// <see cref="DepartmentId"/> is null for a SYSTEM-WIDE (cross-department) report, which is
/// produced only for the in-process BackOffice (Resgrid staff). Department-scoped HTTP callers
/// always receive their own department's data.
///
/// Contains counts only (no PII), which is what makes cross-tenant aggregation safe.
/// </summary>
public class DashboardReport
{
/// <summary>The department this report is scoped to; null = system-wide (BackOffice only).</summary>
public int? DepartmentId { get; set; }

public DateTime StartUtc { get; set; }
public DateTime EndUtc { get; set; }
public ReportGranularity Granularity { get; set; }

/// <summary>When this report was generated (UTC).</summary>
public DateTime GeneratedUtc { get; set; }

/// <summary>
/// IANA/Windows timezone of the requesting context (from the caller's claim), provided so a
/// UI can label/shift the UTC buckets for display. Aggregation itself is always UTC.
/// </summary>
public string TimeZone { get; set; }

public ReportTotals Totals { get; set; } = new ReportTotals();

public List<MetricSeries> Series { get; set; } = new List<MetricSeries>();

public List<Breakdown> Breakdowns { get; set; } = new List<Breakdown>();
}
}
26 changes: 26 additions & 0 deletions Core/Resgrid.Model/Reporting/ExportProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Resgrid.Model.Reporting
{
/// <summary>
/// Selects the field set / standardized mapping used when exporting incident or personnel data.
/// </summary>
public enum ExportProfile
{
/// <summary>Generic Resgrid field set (all available columns).</summary>
[Description("Generic")]
[Display(Name = "Generic")]
Generic = 0,

/// <summary>National Fire Incident Reporting System (NFIRS) field mapping (fire).</summary>
[Description("NFIRS")]
[Display(Name = "NFIRS")]
Nfirs = 1,

/// <summary>National EMS Information System (NEMSIS) field mapping (EMS).</summary>
[Description("NEMSIS")]
[Display(Name = "NEMSIS")]
Nemsis = 2
}
}
24 changes: 24 additions & 0 deletions Core/Resgrid.Model/Reporting/MetricPoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;

namespace Resgrid.Model.Reporting
{
/// <summary>
/// A single point in a time-bucketed metric series. <see cref="BucketUtc"/> is the start of the
/// day or month (UTC). Series are dense: every bucket in the requested window is present, with
/// <see cref="Value"/> = 0 for buckets that had no data.
/// </summary>
public class MetricPoint
{
public DateTime BucketUtc { get; set; }

public long Value { get; set; }

public MetricPoint() { }

public MetricPoint(DateTime bucketUtc, long value)
{
BucketUtc = bucketUtc;
Value = value;
}
}
}
18 changes: 18 additions & 0 deletions Core/Resgrid.Model/Reporting/MetricSeries.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Collections.Generic;

namespace Resgrid.Model.Reporting
{
/// <summary>
/// A named, time-bucketed series (e.g. "calls", "messages", "newUsers"). Points are dense
/// (zero-filled) and ordered ascending by bucket.
/// </summary>
public class MetricSeries
{
/// <summary>Stable machine key, e.g. "calls", "messages", "newUsers".</summary>
public string Key { get; set; }

public ReportGranularity Granularity { get; set; }

public List<MetricPoint> Points { get; set; } = new List<MetricPoint>();
}
}
42 changes: 42 additions & 0 deletions Core/Resgrid.Model/Reporting/ParticipationReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;

namespace Resgrid.Model.Reporting
{
/// <summary>
/// Personnel participation and compliance analytics: per-member call response rate, event/training
/// attendance, and certification expiry status. Especially relevant to volunteer departments.
/// </summary>
public class ParticipationReport
{
public int? DepartmentId { get; set; }
public DateTime StartUtc { get; set; }
public DateTime EndUtc { get; set; }
public DateTime GeneratedUtc { get; set; }

/// <summary>Number of calls in the window used as the denominator for response rates.</summary>
public long CallsInWindow { get; set; }

public List<MemberParticipation> Members { get; set; } = new List<MemberParticipation>();

/// <summary>Count of members with at least one expired certification.</summary>
public int MembersWithExpiredCertifications { get; set; }
}

public class MemberParticipation
{
public string UserId { get; set; }
public string Name { get; set; }

public long CallsResponded { get; set; }

/// <summary>CallsResponded / calls in window (0..1).</summary>
public double ResponseRate { get; set; }

public long EventsAttended { get; set; }
public long TrainingAttended { get; set; }

public int ExpiredCertifications { get; set; }
public int ExpiringCertifications { get; set; }
}
}
19 changes: 19 additions & 0 deletions Core/Resgrid.Model/Reporting/ReportGranularity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Resgrid.Model.Reporting
{
/// <summary>
/// Time-bucketing granularity for reporting series. All buckets are anchored in UTC.
/// </summary>
public enum ReportGranularity
{
[Description("Day")]
[Display(Name = "Day")]
Day = 0,

[Description("Month")]
[Display(Name = "Month")]
Month = 1
}
}
38 changes: 38 additions & 0 deletions Core/Resgrid.Model/Reporting/ReportTotals.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Resgrid.Model.Reporting
{
/// <summary>
/// Scalar totals for a dashboard report. "InWindow" values are scoped to the requested
/// [startUtc, endUtc] range; "AllTime" values are scoped only by department (or system-wide).
/// Personnel/Unit availability counts reflect each resource's current (latest) status mapped to
/// a canonical <see cref="AvailabilityClass"/> via <see cref="AvailabilityMatrix"/>.
/// </summary>
public class ReportTotals
{
// Calls
public long CallsAllTime { get; set; }
public long CallsInWindow { get; set; }
public long ActiveCalls { get; set; }

// Messages
public long MessagesInWindow { get; set; }

// Personnel (current availability)
public int PersonnelTotal { get; set; }
public int PersonnelAvailable { get; set; }
public int PersonnelCommitted { get; set; }
public int PersonnelUnavailable { get; set; }

// Units (current availability)
public int UnitsTotal { get; set; }
public int UnitsAvailable { get; set; }
public int UnitsCommitted { get; set; }
public int UnitsUnavailable { get; set; }

// Users
public long NewUsersInWindow { get; set; }

// System-wide only (null for a department-scoped report)
public long? DepartmentsTotal { get; set; }
public long? NewDepartmentsInWindow { get; set; }
}
}
Loading
Loading