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
This commit is contained in:
1 parent
4e5429929a
commit
3a8e4c40ad
5 files changed
+527
-15
No files matched your search
@@ -0,0 +1,138 @@
|
||||
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)]">·</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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user