Vorgaenge in der WebUI: entscheiden und Stellungnahme anfordern (#689, Stufe 3)
- /api/cases (offen/alle), /api/cases/{id}/decide, /api/cases/{id}/statement, /api/decisions
- Ansicht "Vorgaenge": offene Faelle mit Verlauf je Ebene (Lauf-Links), Frist und
"Mensch gerufen"; Reiter Beschluesse mit Reichweite; Zaehler in der Seitenleiste
- Konzept Abschnitt 5: Umsetzung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
36fe1ad2aa
commit
418a73015d
@@ -0,0 +1,43 @@
|
||||
namespace ClawdDotNet.Web.Contracts;
|
||||
|
||||
/// <summary>#689: A case — escalation or conflict — with everything each level said.</summary>
|
||||
public sealed record CaseDto(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Status,
|
||||
string Subject,
|
||||
string OpenedBy,
|
||||
DateTime OpenedAt,
|
||||
DateTime DeadlineUtc,
|
||||
DateTime? HumanCalledAt,
|
||||
// null: a human holds it.
|
||||
string? Holder,
|
||||
string HolderTaskId,
|
||||
int Level,
|
||||
string OriginTaskId,
|
||||
string OriginRunId,
|
||||
IReadOnlyList<string> Parties,
|
||||
IReadOnlyList<string> Informed,
|
||||
IReadOnlyList<string> WaitingFor,
|
||||
IReadOnlyList<CaseEntryDto> Trail,
|
||||
string? DecisionId);
|
||||
|
||||
public sealed record CaseEntryDto(DateTime At, string Author, string Kind, string Text, string? RunId);
|
||||
|
||||
public sealed record DecisionDto(
|
||||
string Id,
|
||||
string Subject,
|
||||
string Question,
|
||||
string Answer,
|
||||
string Reasoning,
|
||||
string DecidedBy,
|
||||
DateTime DecidedAt,
|
||||
string? ScopeUnitId,
|
||||
// "Purchasing / Team A"; null: binds the whole instance.
|
||||
string? ScopePath,
|
||||
string CaseId,
|
||||
string? RunId);
|
||||
|
||||
public sealed record CaseDecisionInputDto(string? Answer, string? Reasoning);
|
||||
|
||||
public sealed record CaseStatementInputDto(string? AgentId, string? Question);
|
||||
@@ -32,6 +32,7 @@ internal static class ApiEndpoints
|
||||
api.MapAnalytics();
|
||||
api.MapLogs();
|
||||
api.MapOrganisation();
|
||||
api.MapCases();
|
||||
api.MapGet("/team", async (AppHost host, CancellationToken ct) => (await host.Team.SnapshotAsync(ct)).ToDto());
|
||||
|
||||
app.MapEvents();
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.Core.Cases;
|
||||
using ClawdDotNet.Web.Contracts;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace ClawdDotNet.Web.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// #689: Cases and decisions for the human. Reading is the repository's, deciding the desk's —
|
||||
/// the same rules as for an agent, with the human as the one holder above everyone.
|
||||
/// </summary>
|
||||
internal static class CaseEndpoints
|
||||
{
|
||||
public static void MapCases(this RouteGroupBuilder api)
|
||||
{
|
||||
var cases = api.MapGroup("/cases");
|
||||
|
||||
cases.MapGet("/", async (AppHost host, bool? all, CancellationToken ct) =>
|
||||
{
|
||||
if (host.Cases is not { } repo)
|
||||
return Results.Ok(Array.Empty<CaseDto>());
|
||||
var list = all == true ? await repo.ListRecentAsync(200, ct) : await repo.ListOpenAsync(ct);
|
||||
return Results.Ok(list.OrderByDescending(c => c.OpenedAt).Select(ToDto).ToList());
|
||||
});
|
||||
|
||||
cases.MapGet("/{caseId}", async (AppHost host, string caseId, CancellationToken ct) =>
|
||||
host.Cases is { } repo && await repo.GetAsync(caseId, ct) is { } record
|
||||
? Results.Ok(ToDto(record))
|
||||
: Results.NotFound(new OutcomeDto(false, $"No case '{caseId}'.")));
|
||||
|
||||
cases.MapPost("/{caseId}/decide", async (AppHost host, string caseId, CaseDecisionInputDto input, CancellationToken ct) =>
|
||||
{
|
||||
if (host.CaseDesk is not { } desk)
|
||||
return Results.BadRequest(new OutcomeDto(false, "Cases are not available in this instance."));
|
||||
var outcome = await desk.DecideAsync(caseId, CaseRules.Human, input.Answer ?? "", input.Reasoning ?? "", null, ct);
|
||||
return Respond(outcome);
|
||||
});
|
||||
|
||||
cases.MapPost("/{caseId}/statement", async (AppHost host, string caseId, CaseStatementInputDto input, CancellationToken ct) =>
|
||||
{
|
||||
if (host.CaseDesk is not { } desk)
|
||||
return Results.BadRequest(new OutcomeDto(false, "Cases are not available in this instance."));
|
||||
var outcome = await desk.RequestStatementAsync(caseId, input.AgentId ?? "", input.Question ?? "", ct);
|
||||
return Respond(outcome);
|
||||
});
|
||||
|
||||
api.MapGet("/decisions", async (AppHost host, CancellationToken ct) =>
|
||||
{
|
||||
if (host.Cases is not { } repo)
|
||||
return Results.Ok(Array.Empty<DecisionDto>());
|
||||
var org = host.Organisation.Current();
|
||||
return Results.Ok((await repo.ListDecisionsAsync(ct)).Select(d => new DecisionDto(
|
||||
d.Id, d.Subject, d.Question, d.Answer, d.Reasoning, d.DecidedBy, d.DecidedAt,
|
||||
d.ScopeUnitId, d.ScopeUnitId is null ? null : org.PathOf(d.ScopeUnitId), d.CaseId, d.RunId)).ToList());
|
||||
});
|
||||
}
|
||||
|
||||
private static IResult Respond(CaseOutcome outcome)
|
||||
=> outcome.Ok
|
||||
? Results.Ok(new OutcomeDto(true, outcome.Message))
|
||||
: Results.BadRequest(new OutcomeDto(false, outcome.Message));
|
||||
|
||||
private static CaseDto ToDto(CaseRecord c) => new(
|
||||
c.Id, c.Kind.ToString().ToLowerInvariant(), c.Status.ToString().ToLowerInvariant(), c.Subject,
|
||||
c.OpenedBy, c.OpenedAt, c.DeadlineUtc, c.HumanCalledAt, c.HeldByHuman ? null : c.Holder, c.HolderTaskId,
|
||||
c.Level, c.OriginTaskId, c.OriginRunId, c.Parties, c.Informed, c.PendingStatements.Keys.ToList(),
|
||||
c.Trail.Select(e => new CaseEntryDto(e.At, e.Author, e.Kind, e.Text, e.RunId)).ToList(),
|
||||
c.DecisionId);
|
||||
}
|
||||
Reference in New Issue
Block a user