diff --git a/Core/Resgrid.Model/CommandBoards/CommandBoardTemplate.cs b/Core/Resgrid.Model/CommandBoards/CommandBoardTemplate.cs index 5a192dac..c9603c8b 100644 --- a/Core/Resgrid.Model/CommandBoards/CommandBoardTemplate.cs +++ b/Core/Resgrid.Model/CommandBoards/CommandBoardTemplate.cs @@ -88,6 +88,8 @@ public CommandDefinition CreateDefinition(IEnumerable unitTypes, IEnum MaxUnitPersonnel = lane.MaxUnitPersonnel, MinTimeInRole = lane.MinTimeInRole, MaxTimeInRole = lane.MaxTimeInRole, + WorkTimeAmberMinutes = lane.WorkTimeAmberMinutes, + WorkTimeRedMinutes = lane.WorkTimeRedMinutes, RequiredUnitTypes = matchedUnitTypes, RequiredRoles = matchedPersonnelRoles, ForceRequirements = lane.ForceRequirements && (matchedUnitTypes.Count > 0 || matchedPersonnelRoles.Count > 0) diff --git a/Core/Resgrid.Model/CommandBoards/CommandBoardTemplateCatalog.cs b/Core/Resgrid.Model/CommandBoards/CommandBoardTemplateCatalog.cs index 5992b314..016f6d2a 100644 --- a/Core/Resgrid.Model/CommandBoards/CommandBoardTemplateCatalog.cs +++ b/Core/Resgrid.Model/CommandBoards/CommandBoardTemplateCatalog.cs @@ -41,7 +41,8 @@ public static IReadOnlyList Search(string query) private static CommandBoardTemplateLane L(string name, CommandNodeType laneType, string description, string[] unitTypes = null, string[] personnelRoles = null, bool forceRequirements = false, - int minUnits = 0, int maxUnits = 0, int minUnitPersonnel = 0, int maxUnitPersonnel = 0, int minTimeInRole = 0, int maxTimeInRole = 0) + int minUnits = 0, int maxUnits = 0, int minUnitPersonnel = 0, int maxUnitPersonnel = 0, int minTimeInRole = 0, int maxTimeInRole = 0, + int workTimeAmberMinutes = 20, int workTimeRedMinutes = 40) { return new CommandBoardTemplateLane { @@ -56,7 +57,9 @@ private static CommandBoardTemplateLane L(string name, CommandNodeType laneType, MinUnitPersonnel = minUnitPersonnel, MaxUnitPersonnel = maxUnitPersonnel, MinTimeInRole = minTimeInRole, - MaxTimeInRole = maxTimeInRole + MaxTimeInRole = maxTimeInRole, + WorkTimeAmberMinutes = workTimeAmberMinutes, + WorkTimeRedMinutes = workTimeRedMinutes }; } diff --git a/Core/Resgrid.Model/CommandBoards/CommandBoardTemplateLane.cs b/Core/Resgrid.Model/CommandBoards/CommandBoardTemplateLane.cs index c7a0d07b..707d0f8a 100644 --- a/Core/Resgrid.Model/CommandBoards/CommandBoardTemplateLane.cs +++ b/Core/Resgrid.Model/CommandBoards/CommandBoardTemplateLane.cs @@ -47,5 +47,11 @@ public class CommandBoardTemplateLane /// Minutes before an assigned resource shows rotation-due (0 = none). public int MaxTimeInRole { get; set; } + + /// Minutes before the lane work-time indicator turns amber (0 = disabled; default 20). + public int WorkTimeAmberMinutes { get; set; } = 20; + + /// Minutes before the lane work-time indicator turns red (0 = disabled; default 40). + public int WorkTimeRedMinutes { get; set; } = 40; } } diff --git a/Core/Resgrid.Model/CommandDefinitionRole.cs b/Core/Resgrid.Model/CommandDefinitionRole.cs index c460dd12..314f0216 100644 --- a/Core/Resgrid.Model/CommandDefinitionRole.cs +++ b/Core/Resgrid.Model/CommandDefinitionRole.cs @@ -35,6 +35,12 @@ public class CommandDefinitionRole : IEntity public int MaxTimeInRole { get; set; } + /// Minutes on the lane before the work-time indicator turns amber (0 = disabled; seeded onto runtime nodes). + public int WorkTimeAmberMinutes { get; set; } + + /// Minutes on the lane before the work-time indicator turns red (0 = disabled; seeded onto runtime nodes). + public int WorkTimeRedMinutes { get; set; } + public bool ForceRequirements { get; set; } /// diff --git a/Core/Resgrid.Model/IncidentCommand/CommandStructureNode.cs b/Core/Resgrid.Model/IncidentCommand/CommandStructureNode.cs index 30034d4c..c1df14ae 100644 --- a/Core/Resgrid.Model/IncidentCommand/CommandStructureNode.cs +++ b/Core/Resgrid.Model/IncidentCommand/CommandStructureNode.cs @@ -57,6 +57,12 @@ public class CommandStructureNode : IEntity, IChangeTracked /// Maximum minutes a resource should work this lane before rotation (0 = none; surfaced as rotation-due). public int MaxTimeInRole { get; set; } + /// Minutes on this lane before the work-time indicator turns amber (0 = disabled; client defaults to 20). + public int WorkTimeAmberMinutes { get; set; } + + /// Minutes on this lane before the work-time indicator turns red (0 = disabled; client defaults to 40). + public int WorkTimeRedMinutes { get; set; } + /// When true, unmet lane requirements block assignment instead of warning. Seeded from the template role. public bool ForceRequirements { get; set; } diff --git a/Core/Resgrid.Services/IncidentCommandService.cs b/Core/Resgrid.Services/IncidentCommandService.cs index e9f5c85a..b8c505f6 100644 --- a/Core/Resgrid.Services/IncidentCommandService.cs +++ b/Core/Resgrid.Services/IncidentCommandService.cs @@ -215,6 +215,8 @@ public IncidentCommandService( MaxUnits = role.MaxUnits, MinTimeInRole = role.MinTimeInRole, MaxTimeInRole = role.MaxTimeInRole, + WorkTimeAmberMinutes = role.WorkTimeAmberMinutes, + WorkTimeRedMinutes = role.WorkTimeRedMinutes, ForceRequirements = role.ForceRequirements }; diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0103_AddLaneWorkTimeThresholds.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0103_AddLaneWorkTimeThresholds.cs new file mode 100644 index 00000000..7fdd686b --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0103_AddLaneWorkTimeThresholds.cs @@ -0,0 +1,61 @@ +using FluentMigrator; + +namespace Resgrid.Providers.Migrations.Migrations +{ + /// + /// Per-lane work-time (crew fatigue) indicator thresholds: minutes before the lane work light + /// turns amber/red (0 = disabled; clients fall back to their own defaults). Denormalized onto + /// runtime nodes from the template role at seeding, like the other lane limits. + /// + [Migration(103)] + public class M0103_AddLaneWorkTimeThresholds : Migration + { + public override void Up() + { + if (Schema.Table("CommandStructureNodes").Exists()) + { + foreach (var column in new[] { "WorkTimeAmberMinutes", "WorkTimeRedMinutes" }) + { + if (!Schema.Table("CommandStructureNodes").Column(column).Exists()) + { + Alter.Table("CommandStructureNodes") + .AddColumn(column).AsInt32().NotNullable().WithDefaultValue(0); + } + } + } + + if (Schema.Table("CommandDefinitionRoles").Exists()) + { + foreach (var column in new[] { "WorkTimeAmberMinutes", "WorkTimeRedMinutes" }) + { + if (!Schema.Table("CommandDefinitionRoles").Column(column).Exists()) + { + Alter.Table("CommandDefinitionRoles") + .AddColumn(column).AsInt32().NotNullable().WithDefaultValue(0); + } + } + } + } + + public override void Down() + { + if (Schema.Table("CommandStructureNodes").Exists()) + { + foreach (var column in new[] { "WorkTimeAmberMinutes", "WorkTimeRedMinutes" }) + { + if (Schema.Table("CommandStructureNodes").Column(column).Exists()) + Delete.Column(column).FromTable("CommandStructureNodes"); + } + } + + if (Schema.Table("CommandDefinitionRoles").Exists()) + { + foreach (var column in new[] { "WorkTimeAmberMinutes", "WorkTimeRedMinutes" }) + { + if (Schema.Table("CommandDefinitionRoles").Column(column).Exists()) + Delete.Column(column).FromTable("CommandDefinitionRoles"); + } + } + } + } +} diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0103_AddLaneWorkTimeThresholdsPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0103_AddLaneWorkTimeThresholdsPg.cs new file mode 100644 index 00000000..56fd27a4 --- /dev/null +++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0103_AddLaneWorkTimeThresholdsPg.cs @@ -0,0 +1,61 @@ +using FluentMigrator; + +namespace Resgrid.Providers.MigrationsPg.Migrations +{ + /// + /// Per-lane work-time (crew fatigue) indicator thresholds: minutes before the lane work light + /// turns amber/red (0 = disabled; clients fall back to their own defaults). Denormalized onto + /// runtime nodes from the template role at seeding, like the other lane limits. + /// + [Migration(103)] + public class M0103_AddLaneWorkTimeThresholdsPg : Migration + { + public override void Up() + { + if (Schema.Table("commandstructurenodes").Exists()) + { + foreach (var column in new[] { "worktimeamberminutes", "worktimeredminutes" }) + { + if (!Schema.Table("commandstructurenodes").Column(column).Exists()) + { + Alter.Table("commandstructurenodes") + .AddColumn(column).AsInt32().NotNullable().WithDefaultValue(0); + } + } + } + + if (Schema.Table("commanddefinitionroles").Exists()) + { + foreach (var column in new[] { "worktimeamberminutes", "worktimeredminutes" }) + { + if (!Schema.Table("commanddefinitionroles").Column(column).Exists()) + { + Alter.Table("commanddefinitionroles") + .AddColumn(column).AsInt32().NotNullable().WithDefaultValue(0); + } + } + } + } + + public override void Down() + { + if (Schema.Table("commandstructurenodes").Exists()) + { + foreach (var column in new[] { "worktimeamberminutes", "worktimeredminutes" }) + { + if (Schema.Table("commandstructurenodes").Column(column).Exists()) + Delete.Column(column).FromTable("commandstructurenodes"); + } + } + + if (Schema.Table("commanddefinitionroles").Exists()) + { + foreach (var column in new[] { "worktimeamberminutes", "worktimeredminutes" }) + { + if (Schema.Table("commanddefinitionroles").Column(column).Exists()) + Delete.Column(column).FromTable("commanddefinitionroles"); + } + } + } + } +} diff --git a/Web/Resgrid.Web.Services/Controllers/v4/CommandsController.cs b/Web/Resgrid.Web.Services/Controllers/v4/CommandsController.cs index ce9837ab..4781beb8 100644 --- a/Web/Resgrid.Web.Services/Controllers/v4/CommandsController.cs +++ b/Web/Resgrid.Web.Services/Controllers/v4/CommandsController.cs @@ -173,6 +173,8 @@ public async Task> SaveCommand([FromBody] SaveComman MaxUnits = lane.MaxUnits, MinTimeInRole = lane.MinTimeInRole, MaxTimeInRole = lane.MaxTimeInRole, + WorkTimeAmberMinutes = lane.WorkTimeAmberMinutes, + WorkTimeRedMinutes = lane.WorkTimeRedMinutes, ForceRequirements = lane.ForceRequirements }; @@ -253,6 +255,8 @@ private static CommandResultData ConvertCommandData(CommandDefinition command) MaxUnits = lane.MaxUnits, MinTimeInRole = lane.MinTimeInRole, MaxTimeInRole = lane.MaxTimeInRole, + WorkTimeAmberMinutes = lane.WorkTimeAmberMinutes, + WorkTimeRedMinutes = lane.WorkTimeRedMinutes, ForceRequirements = lane.ForceRequirements, RequiredUnitTypes = lane.RequiredUnitTypes?.Select(x => x.UnitTypeId).ToList() ?? new List(), RequiredPersonnelRoles = lane.RequiredRoles?.Select(x => x.PersonnelRoleId).ToList() ?? new List() diff --git a/Web/Resgrid.Web.Services/Models/v4/Commands/CommandModels.cs b/Web/Resgrid.Web.Services/Models/v4/Commands/CommandModels.cs index a4c2967a..99181fc6 100644 --- a/Web/Resgrid.Web.Services/Models/v4/Commands/CommandModels.cs +++ b/Web/Resgrid.Web.Services/Models/v4/Commands/CommandModels.cs @@ -46,6 +46,8 @@ public class CommandRoleResultData public int MaxUnits { get; set; } public int MinTimeInRole { get; set; } public int MaxTimeInRole { get; set; } + public int WorkTimeAmberMinutes { get; set; } + public int WorkTimeRedMinutes { get; set; } /// Lane identification color (hex, e.g. "#e74c3c"); null = default styling. public string Color { get; set; } @@ -110,6 +112,8 @@ public class SaveCommandLaneInput public int MaxUnits { get; set; } public int MinTimeInRole { get; set; } public int MaxTimeInRole { get; set; } + public int WorkTimeAmberMinutes { get; set; } + public int WorkTimeRedMinutes { get; set; } /// Lane identification color (hex, e.g. "#e74c3c"); null = default styling. public string Color { get; set; }