Theme 2: cross-division delivery pipeline (change requests)

A customer change request now flows through a guarded commercial pipeline:
Requested -> Estimated -> Approved -> Paid -> Live. The cross-division work and
its dependencies live on the request's steps (a division's slice + hours +
an optional depends-on link), and estimating sums the steps into a total. Each
transition is guarded on the ChangeRequest aggregate, so it can only move
forward in order; guard violations surface as 400s.

- Domain: ChangeRequest + ChangeRequestStep aggregates with stage guards
- Persistence: two tables + EF migration (applied)
- Endpoints under /api/orgboard/change-requests: create/list/detail, add/advance
  steps, and estimate/approve/pay/go-live/reject (reads need board-view,
  commercial actions are owner-level)
- New Delivery pipeline page: request list with stage + step progress, a detail
  drawer with a stage stepper, the next commercial action, quote entry, and a
  per-division step breakdown with dependencies

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-17 07:47:57 +03:30
parent 5c2b697b66
commit c12935ad74
12 changed files with 1816 additions and 0 deletions
@@ -170,6 +170,116 @@ namespace TeamUp.Modules.OrgBoard.Persistence.Migrations
b.ToTable("agent_profiles", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.ChangeRequest", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal?>("Amount")
.HasPrecision(12, 2)
.HasColumnType("numeric(12,2)");
b.Property<DateTimeOffset?>("ApprovedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("Currency")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("CustomerName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<decimal?>("EstimateHours")
.HasPrecision(9, 2)
.HasColumnType("numeric(9,2)");
b.Property<DateTimeOffset?>("EstimatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("LiveAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<DateTimeOffset?>("PaidAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<DateTimeOffset>("UpdatedAtUtc")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("OrganizationId");
b.ToTable("change_requests", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.ChangeRequestStep", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ChangeRequestId")
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("DependsOnStepId")
.HasColumnType("uuid");
b.Property<Guid?>("DivisionId")
.HasColumnType("uuid");
b.Property<decimal>("EstimateHours")
.HasPrecision(9, 2)
.HasColumnType("numeric(9,2)");
b.Property<int>("Order")
.HasColumnType("integer");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<DateTimeOffset>("UpdatedAtUtc")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ChangeRequestId");
b.ToTable("change_request_steps", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Division", b =>
{
b.Property<Guid>("Id")