Files
AgentCompany/src/renderer/components/execution/IncidentList.tsx
T
ZacharyZhang-NY 3a8e4c40ad feat(05-03): PolicyList, PolicyFormModal, IncidentList and costs tab navigation
- Create PolicyList with utilization bars, edit/delete, empty state
- Create PolicyFormModal with scope selector, dollar amount, warn/hard-stop toggles
- Create IncidentList with status filtering and manual resolve action
- Add tabbed navigation (Cost Entries | Policies | Incidents) to costs section
- Export new components from execution barrel file
2026-03-26 21:13:17 -04:00

139 lines
6.0 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import { CheckCircle, Warning } from "@phosphor-icons/react";
import type { BudgetIncidentRecord } from "@shared/types";
import { ActionButton, EmptyState, ListFrame, ListRow } from "../ui";
import { formatTime } from "../../lib/formatters";
interface IncidentListProps {
companyId: string;
agents: Array<{ id: string; name: string }>;
projects: Array<{ id: string; name: string }>;
}
const STATUS_STYLES: Record<string, string> = {
open: "text-[color:var(--danger)] bg-[color:var(--danger-soft)]",
resolved: "text-[color:var(--success)] bg-[color:var(--success-soft)]",
auto_resolved_limit_increase: "text-[color:var(--accent)] bg-[color:var(--accent-soft)]",
auto_closed: "text-[color:var(--muted-strong)] bg-[color:var(--panel-soft)]",
};
const STATUS_LABELS: Record<string, string> = {
open: "Open",
resolved: "Resolved",
auto_resolved_limit_increase: "Auto-resolved",
auto_closed: "Auto-closed",
};
export function IncidentList({ companyId, agents, projects }: IncidentListProps) {
const [incidents, setIncidents] = useState<BudgetIncidentRecord[]>([]);
const [filter, setFilter] = useState<"all" | "open">("open");
const loadData = useCallback(async () => {
const result = await window.agentCompany.listBudgetIncidents({ companyId, status: filter });
if (result.ok) setIncidents(result.data);
}, [companyId, filter]);
useEffect(() => { void loadData(); }, [loadData]);
function getScopeName(scopeType: string, scopeId: string): string {
if (scopeType === "company") return "Company";
if (scopeType === "agent") {
const agent = agents.find((a) => a.id === scopeId);
return agent ? agent.name : scopeId.slice(0, 8);
}
if (scopeType === "project") {
const project = projects.find((p) => p.id === scopeId);
return project ? project.name : scopeId.slice(0, 8);
}
return scopeId.slice(0, 8);
}
async function handleResolve(incidentId: string) {
const result = await window.agentCompany.resolveBudgetIncident({ companyId, incidentId });
if (result.ok) void loadData();
}
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="flex items-center justify-between px-4 py-3">
<h3 className="text-[15px] font-semibold text-[color:var(--text)]">Budget Incidents</h3>
<div className="flex items-center gap-1 rounded-[6px] bg-[color:var(--panel-soft)] p-0.5">
{(["open", "all"] as const).map((f) => (
<button
key={f}
type="button"
onClick={() => setFilter(f)}
className={`rounded-[5px] px-3 py-1 text-[12px] font-medium transition ${
filter === f
? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]"
}`}
>
{f === "open" ? "Open" : "All"}
</button>
))}
</div>
</div>
<div className="flex-1 overflow-y-auto px-4 pb-4">
{incidents.length === 0 ? (
<EmptyState
title="No budget incidents"
detail={filter === "open" ? "No open incidents. All spending is within limits." : "No incidents recorded yet."}
/>
) : (
<ListFrame>
{incidents.map((incident) => {
const isWarn = incident.thresholdType === "warn_threshold";
return (
<ListRow key={incident.id}>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] ${
isWarn
? "text-[color:var(--warn)] bg-[color:var(--warn-soft)]"
: "text-[color:var(--danger)] bg-[color:var(--danger-soft)]"
}`}
>
{isWarn ? <Warning size={10} weight="bold" /> : null}
{isWarn ? "Warning" : "Hard Stop"}
</span>
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] ${STATUS_STYLES[incident.status] ?? STATUS_STYLES.open}`}>
{STATUS_LABELS[incident.status] ?? incident.status}
</span>
</div>
<div className="mt-1.5 flex items-center gap-2 text-[12px] text-[color:var(--muted-strong)]">
<span className="capitalize">{incident.scopeType}</span>
<span className="text-[color:var(--muted)]">&middot;</span>
<span className="font-medium text-[color:var(--text)]">{getScopeName(incident.scopeType, incident.scopeId)}</span>
</div>
<div className="mt-1 flex items-center gap-3 text-[11px] text-[color:var(--muted)]">
<span className="font-[family-name:var(--font-mono)]" style={{ fontVariantNumeric: "tabular-nums" }}>
${(incident.amountObserved / 100).toFixed(2)} / ${(incident.amountLimit / 100).toFixed(2)}
</span>
<span>{formatTime(incident.windowStart)} - {formatTime(incident.windowEnd)}</span>
</div>
</div>
{incident.status === "open" ? (
<ActionButton
label="Resolve"
icon={CheckCircle}
onClick={() => void handleResolve(incident.id)}
/>
) : null}
</div>
</ListRow>
);
})}
</ListFrame>
)}
</div>
</div>
);
}