Organigramm: Lage je Abteilung in der Teamuebersicht (#688)

- TeamMember: Laeufe und Kosten heute, offene und blockierte Aufgaben je Agent
  (eine Abfrage fuer alle, nicht eine je Agent)
- TeamUnit: dieselben Werte samt Untereinheiten, offene Vorgaenge aus der Einheit,
  heutige Kostenmeldung (#699)
- team.js: Lagezeile auf jeder Karte und in der Seitenleiste, Instanz summiert;
  Spruenge zu Kosten je Einheit und zu den Vorgaengen; i18n en/de
- Tests: TeamOverviewTests (+2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-01 09:50:17 +02:00
co-authored by Claude Opus 5.5
parent 888369698d
commit f649f2fd7a
10 changed files with 207 additions and 15 deletions
@@ -186,4 +186,63 @@ public sealed class TeamOverviewTests
TeamOverview.Readable(elsewhere, configured).ShouldBe(elsewhere);
TeamOverview.Readable(elsewhere, []).ShouldBe(elsewhere);
}
// ═══════════════════ the department's situation (#688) ═══════════════════
[Fact]
public void A_departments_day_is_added_up_along_the_tree()
{
var units = TeamOverview.RollUp(Shop(),
[
Member("marketing-lead", "marketing", TeamState.Idle) with { RunsToday = 2, CostToday = 0.5m, OpenTasks = 1 },
Member("ads", "amazon", TeamState.Idle) with { RunsToday = 5, CostToday = 1.25m, OpenTasks = 3, BlockedTasks = 1 }
]);
var marketing = units.Single(u => u.Id == "marketing");
marketing.RunsToday.ShouldBe(7);
marketing.CostToday.ShouldBe(1.75m);
marketing.OpenTasks.ShouldBe(4);
marketing.BlockedTasks.ShouldBe(1);
units.Single(u => u.Id == "amazon").CostToday.ShouldBe(1.25m);
}
[Fact]
public async Task The_snapshot_carries_runs_cost_tasks_cases_and_the_cost_alert_of_today()
{
var directory = Path.Combine(Path.GetTempPath(), "clawd-tests", "team-" + Guid.NewGuid().ToString("N")[..8]);
try
{
var storage = new Core.Storage.SqliteStorage(Path.Combine(directory, "state.db"));
var usage = new Core.Accounting.SqliteUsageRepository(storage);
var board = new Core.Tasks.TaskboardService(new Core.Tasks.SqliteTaskRepository(storage), Path.Combine(directory, "tasks"));
var cases = new Core.Cases.SqliteCaseRepository(storage);
await usage.RecordAsync(new Core.Accounting.RunUsage { AgentId = "ads", Model = "m", CostUsd = 0.4m, CostIsKnown = true, Status = "Completed", OccurredAt = DateTime.Now }, default);
await board.CreateAsync(new Core.Tasks.TaskItem { Title = "Bid for spring", Assignee = "@ads" }, default);
await board.CreateAsync(new Core.Tasks.TaskItem { Title = "Waiting", Assignee = "@ads", Status = Core.Tasks.TaskItemStatus.Blocked }, default);
await cases.SaveAsync(new Core.Cases.CaseRecord { Id = "c-1", Subject = "Field", OpenedBy = "ads", OpenedUnitId = "amazon", OpenedAt = DateTime.UtcNow }, default);
var today = DateOnly.FromDateTime(DateTime.Now).ToString("yyyy-MM-dd");
var overview = new TeamOverview(null, null, null, usage, () => Shop().Agents.ToList(),
organisation: Shop, board: board, cases: cases,
costAlerts: _ => Task.FromResult<IReadOnlyList<Core.Accounting.RaisedCostAlert>>(
[new Core.Accounting.RaisedCostAlert(today, "amazon", Core.Accounting.CostAlertKind.Deviation, "x", DateTime.UtcNow)]));
var snapshot = await overview.SnapshotAsync(default);
var ads = snapshot.Members.Single(m => m.AgentId == "ads");
ads.RunsToday.ShouldBe(1);
ads.CostToday.ShouldBe(0.4m);
ads.OpenTasks.ShouldBe(1);
ads.BlockedTasks.ShouldBe(1);
var marketing = snapshot.Units!.Single(u => u.Id == "marketing");
marketing.OpenCases.ShouldBe(1, "a case raised in amazon lies inside marketing");
snapshot.Units!.Single(u => u.Id == "amazon").CostAlert.ShouldBe("Deviation");
marketing.CostAlert.ShouldBeNull("the alert was raised on amazon, not on marketing");
}
finally
{
Core.Storage.SqliteStorage.ReleasePooledConnections(directory);
try { Directory.Delete(directory, recursive: true); } catch { /* cleanup is secondary */ }
}
}
}