Runtime Assignments are now created in the database when the activity is planned.

This commit is contained in:
Colin Dawson 2026-03-15 23:07:07 +00:00
parent 7048594ea9
commit a12731142c
13 changed files with 2579 additions and 14 deletions

View File

@ -75,4 +75,5 @@ public class EsuiteDatabaseDbContext : SunDatabaseEntityContext, IEsuiteDatabase
public DbSet<WorkflowVersion> WorkflowVersions { get; set; } public DbSet<WorkflowVersion> WorkflowVersions { get; set; }
public DbSet<Activity> Activities { get; set; } public DbSet<Activity> Activities { get; set; }
public DbSet<ActivityTask> ActivityTasks { get; set; } public DbSet<ActivityTask> ActivityTasks { get; set; }
public DbSet<ActivityAssignment> ActivityAssignments { get; set; }
} }

View File

@ -0,0 +1,53 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using e_suite.Database.Audit.Attributes;
using e_suite.Database.Core.Models;
using e_suite.Database.Core.Tables.Domain;
using e_suite.Database.Core.Tables.UserManager;
using eSuite.Core.Workflow;
using Microsoft.EntityFrameworkCore;
namespace e_suite.Database.Core.Tables.Activity;
[DisplayName("Activity Assignment")]
[Table("ActivityAssignments", Schema = "Activity")]
[Index(nameof(Guid), IsUnique = true)]
public class ActivityAssignment : IGeneralId, ISoftDeletable
{
[Key]
public long Id { get; set; }
public Guid Guid { get; set; }
[AuditSoftDelete(true)]
[Required]
[DefaultValue(false)]
public bool Deleted { get; set; }
public long TaskId { get; set; }
public Raci Raci { get; set; }
public long? RoleId { get; set; }
public long? UserId { get; set; }
public bool AllowNoVerdict { get; set; } = false;
public bool Bypassable { get; set; } = false;
[AuditLastUpdated]
public DateTimeOffset LastUpdated { get; set; }
[AuditParent]
[ForeignKey(nameof(TaskId))]
public virtual ActivityTask Task { get; set; } = null!;
[ForeignKey(nameof(RoleId))]
public virtual Role Role { get; set; } = null!;
[ForeignKey(nameof(UserId))]
public virtual User User { get; set; } = null!;
}

View File

@ -68,6 +68,8 @@ public class ActivityTask : IGeneralId, ISoftDeletable
[ForeignKey(nameof(ParentId))] [ForeignKey(nameof(ParentId))]
public virtual ActivityTask ParentTask { get; set; } = null!; public virtual ActivityTask ParentTask { get; set; } = null!;
public ICollection<ActivityAssignment> Assignments { get; set; } = [];
public ICollection<ActivityTask> Tasks { get; set; } = []; public ICollection<ActivityTask> Tasks { get; set; } = [];
public List<string> Outcomes { get; set; } = []; public List<string> Outcomes { get; set; } = [];

View File

@ -6,4 +6,5 @@ public interface IActivity : IDatabaseCore
{ {
DbSet<Activity> Activities { get; set; } DbSet<Activity> Activities { get; set; }
DbSet<ActivityTask> ActivityTasks { get; set; } DbSet<ActivityTask> ActivityTasks { get; set; }
DbSet<ActivityAssignment> ActivityAssignments { get; set; }
} }

View File

@ -0,0 +1,89 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace esuite.Database.SqlServer.Migrations
{
/// <inheritdoc />
public partial class addingactivityassignments : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ActivityAssignments",
schema: "Activity",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Guid = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Deleted = table.Column<bool>(type: "bit", nullable: false),
TaskId = table.Column<long>(type: "bigint", nullable: false),
Raci = table.Column<int>(type: "int", nullable: false),
RoleId = table.Column<long>(type: "bigint", nullable: true),
UserId = table.Column<long>(type: "bigint", nullable: true),
AllowNoVerdict = table.Column<bool>(type: "bit", nullable: false),
Bypassable = table.Column<bool>(type: "bit", nullable: false),
LastUpdated = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ActivityAssignments", x => x.Id);
table.ForeignKey(
name: "FK_ActivityAssignments_ActivityTasks_TaskId",
column: x => x.TaskId,
principalSchema: "Activity",
principalTable: "ActivityTasks",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ActivityAssignments_Roles_RoleId",
column: x => x.RoleId,
principalSchema: "Domain",
principalTable: "Roles",
principalColumn: "Id");
table.ForeignKey(
name: "FK_ActivityAssignments_Users_UserId",
column: x => x.UserId,
principalSchema: "UserManager",
principalTable: "Users",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_ActivityAssignments_Guid",
schema: "Activity",
table: "ActivityAssignments",
column: "Guid",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ActivityAssignments_RoleId",
schema: "Activity",
table: "ActivityAssignments",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "IX_ActivityAssignments_TaskId",
schema: "Activity",
table: "ActivityAssignments",
column: "TaskId");
migrationBuilder.CreateIndex(
name: "IX_ActivityAssignments_UserId",
schema: "Activity",
table: "ActivityAssignments",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ActivityAssignments",
schema: "Activity");
}
}
}

View File

@ -154,6 +154,55 @@ namespace esuite.Database.SqlServer.Migrations
b.ToTable("Activities", "Activity"); b.ToTable("Activities", "Activity");
}); });
modelBuilder.Entity("e_suite.Database.Core.Tables.Activity.ActivityAssignment", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<bool>("AllowNoVerdict")
.HasColumnType("bit");
b.Property<bool>("Bypassable")
.HasColumnType("bit");
b.Property<bool>("Deleted")
.HasColumnType("bit");
b.Property<Guid>("Guid")
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("LastUpdated")
.HasColumnType("datetimeoffset");
b.Property<int>("Raci")
.HasColumnType("int");
b.Property<long?>("RoleId")
.HasColumnType("bigint");
b.Property<long>("TaskId")
.HasColumnType("bigint");
b.Property<long?>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("Guid")
.IsUnique();
b.HasIndex("RoleId");
b.HasIndex("TaskId");
b.HasIndex("UserId");
b.ToTable("ActivityAssignments", "Activity");
});
modelBuilder.Entity("e_suite.Database.Core.Tables.Activity.ActivityTask", b => modelBuilder.Entity("e_suite.Database.Core.Tables.Activity.ActivityTask", b =>
{ {
b.Property<long>("Id") b.Property<long>("Id")
@ -1785,6 +1834,29 @@ namespace esuite.Database.SqlServer.Migrations
b.Navigation("WorkflowVersion"); b.Navigation("WorkflowVersion");
}); });
modelBuilder.Entity("e_suite.Database.Core.Tables.Activity.ActivityAssignment", b =>
{
b.HasOne("e_suite.Database.Core.Tables.Domain.Role", "Role")
.WithMany()
.HasForeignKey("RoleId");
b.HasOne("e_suite.Database.Core.Tables.Activity.ActivityTask", "task")
.WithMany()
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("e_suite.Database.Core.Tables.UserManager.User", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("Role");
b.Navigation("User");
b.Navigation("task");
});
modelBuilder.Entity("e_suite.Database.Core.Tables.Activity.ActivityTask", b => modelBuilder.Entity("e_suite.Database.Core.Tables.Activity.ActivityTask", b =>
{ {
b.HasOne("e_suite.Database.Core.Tables.Activity.Activity", "Activity") b.HasOne("e_suite.Database.Core.Tables.Activity.Activity", "Activity")

View File

@ -43,7 +43,9 @@ public class ActivityRepository : RepositoryBase, IActivityRepository
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
DatabaseDbContext.ActivityTasks.AddRange(activityTasks); var enumerable = activityTasks as ActivityTask[] ?? activityTasks.ToArray();
DatabaseDbContext.ActivityTasks.AddRange(enumerable);
DatabaseDbContext.ActivityAssignments.AddRange(enumerable.SelectMany( x => x.Assignments));
await DatabaseDbContext.SaveChangesAsync(auditUserDetails, cancellationToken); await DatabaseDbContext.SaveChangesAsync(auditUserDetails, cancellationToken);
} }

View File

@ -1,13 +1,16 @@
using e_suite.API.Common.repository; using e_suite.API.Common.repository;
using e_suite.Database.Audit; using e_suite.Database.Audit;
using e_suite.Database.Core.Tables.Activity; using e_suite.Database.Core.Tables.Activity;
using e_suite.Database.Core.Tables.Domain;
using e_suite.Messaging.Common; using e_suite.Messaging.Common;
using e_suite.Workflow.Core; using e_suite.Workflow.Core;
using e_suite.Workflow.Core.Extensions; using e_suite.Workflow.Core.Extensions;
using e_suite.Workflow.Core.Interfaces;
using eSuite.Core.Clock; using eSuite.Core.Clock;
using eSuite.Core.Enums; using eSuite.Core.Enums;
using eSuite.Core.Miscellaneous; using eSuite.Core.Miscellaneous;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections;
namespace e_suite.Service.WorkflowProcessor; namespace e_suite.Service.WorkflowProcessor;
@ -168,8 +171,11 @@ public class WorkflowProcessor : IWorkflowProcessor
TaskType = task.GetType().FullName!, TaskType = task.GetType().FullName!,
TaskName = task.Name, TaskName = task.Name,
ActivityOrdinal = activityOrdinalCounter++, ActivityOrdinal = activityOrdinalCounter++,
}; };
if (task is IAssignees assignableTask)
activityTask.Assignments = await GenerateAssignmentForTask(activityTask, assignableTask, auditUserDetails, cancellationToken );
activityTasks.Add(activityTask); activityTasks.Add(activityTask);
} }
@ -177,4 +183,47 @@ public class WorkflowProcessor : IWorkflowProcessor
return activityTasks; return activityTasks;
} }
private async Task<ICollection<ActivityAssignment>> GenerateAssignmentForTask(ActivityTask activityTask, IAssignees task, AuditUserDetails auditUserDetails, CancellationToken cancellationToken)
{
// Find the IAssignees<T> interface implemented by this instance
var typedInterface = task.GetType()
.GetInterfaces()
.FirstOrDefault(i =>
i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IAssignees<>));
if (typedInterface == null)
throw new InvalidOperationException("Task does not implement IAssignees<T>.");
// Get the Assignees property
var assigneesProperty = typedInterface.GetProperty("Assignees");
var assignees = (IEnumerable)assigneesProperty.GetValue(task);
var results = new List<ActivityAssignment>();
foreach (var assignee in assignees)
{
var typedAssignee = (ITaskAssignee)assignee;
var activityAssignment = new ActivityAssignment
{
Guid = Guid.NewGuid(),
Raci = typedAssignee.Raci,
Role = typedAssignee.Role!,
User = typedAssignee.User!,
};
if (typedAssignee is IApprovalTaskAssignee approvalAssignee)
{
activityAssignment.Bypassable = approvalAssignee.Bypassable;
activityAssignment.AllowNoVerdict = approvalAssignee.AllowNoVerdict;
}
results.Add(activityAssignment);
}
return results;
}
} }

View File

@ -1,9 +0,0 @@
namespace e_suite.Workflow.Core.Enums;
public enum Raci
{
Responsible,
Accountable,
Consulted,
Informed
}

View File

@ -3,8 +3,13 @@ using System.ComponentModel.DataAnnotations;
namespace e_suite.Workflow.Core.Interfaces; namespace e_suite.Workflow.Core.Interfaces;
public interface IAssignees
{
}
[TaskCapability] [TaskCapability]
public interface IAssignees<T> where T : ITaskAssignee public interface IAssignees<T> : IAssignees where T : ITaskAssignee
{ {
[Required] [Required]
List<T> Assignees { get; set; } List<T> Assignees { get; set; }

View File

@ -1,7 +1,7 @@
using e_suite.Database.Core.Tables.Domain; using e_suite.Database.Core.Tables.Domain;
using e_suite.Database.Core.Tables.UserManager; using e_suite.Database.Core.Tables.UserManager;
using e_suite.Workflow.Core.Attributes; using e_suite.Workflow.Core.Attributes;
using e_suite.Workflow.Core.Enums; using eSuite.Core.Workflow;
namespace e_suite.Workflow.Core.Interfaces; namespace e_suite.Workflow.Core.Interfaces;

View File

@ -1,8 +1,8 @@
using e_suite.Database.Core.Tables.Domain; using e_suite.Database.Core.Tables.Domain;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using e_suite.Database.Core.Tables.UserManager; using e_suite.Database.Core.Tables.UserManager;
using e_suite.Workflow.Core.Enums;
using e_suite.Workflow.Core.Interfaces; using e_suite.Workflow.Core.Interfaces;
using eSuite.Core.Workflow;
namespace e_suite.Workflow.Core; namespace e_suite.Workflow.Core;