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
+53
-15
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import {
|
||||
CaretUpDown,
|
||||
@@ -60,6 +60,8 @@ import {
|
||||
WorkspaceInspector,
|
||||
WorkspaceList,
|
||||
} from "./components/execution";
|
||||
import { PolicyList } from "./components/execution/PolicyList";
|
||||
import { IncidentList } from "./components/execution/IncidentList";
|
||||
import {
|
||||
AgentCreator,
|
||||
AgentEditor,
|
||||
@@ -175,6 +177,8 @@ export function App() {
|
||||
const browserActions = useAppStore((s) => s.browserActions);
|
||||
const setBrowserActions = useAppStore((s) => s.setBrowserActions);
|
||||
|
||||
const [costsTab, setCostsTab] = useState<"costs" | "policies" | "incidents">("costs");
|
||||
|
||||
const clearCompanyScopedUiState = useCallback(() => {
|
||||
const store = useAppStore.getState();
|
||||
store.clearCompanyScopedSelections();
|
||||
@@ -1108,20 +1112,54 @@ export function App() {
|
||||
) : null}
|
||||
|
||||
{section === "costs" ? (
|
||||
<EntityWorkspace
|
||||
title="Costs"
|
||||
eyebrow={undefined}
|
||||
primary={
|
||||
<Panel title="Recent cost entries">
|
||||
<CostList costs={companyCosts} agents={companyAgents} runs={runs} />
|
||||
</Panel>
|
||||
}
|
||||
secondary={
|
||||
<Panel title="Budget posture">
|
||||
<BudgetSummary agents={companyAgents} costs={companyCosts} />
|
||||
</Panel>
|
||||
}
|
||||
/>
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Tab bar */}
|
||||
<div className="flex items-center gap-1 border-b border-[color:var(--line)] px-4 pt-2">
|
||||
{(["costs", "policies", "incidents"] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
onClick={() => setCostsTab(tab)}
|
||||
className={`rounded-t px-3 py-1.5 text-[13px] font-medium transition-colors ${
|
||||
costsTab === tab
|
||||
? "border-b-2 border-[color:var(--accent)] text-[color:var(--text)]"
|
||||
: "text-[color:var(--muted)] hover:text-[color:var(--text)]"
|
||||
}`}
|
||||
>
|
||||
{tab === "costs" ? "Cost Entries" : tab === "policies" ? "Policies" : "Incidents"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Tab content */}
|
||||
{costsTab === "costs" ? (
|
||||
<EntityWorkspace
|
||||
title="Costs"
|
||||
eyebrow={undefined}
|
||||
primary={
|
||||
<Panel title="Recent cost entries">
|
||||
<CostList costs={companyCosts} agents={companyAgents} runs={runs} />
|
||||
</Panel>
|
||||
}
|
||||
secondary={
|
||||
<Panel title="Budget posture">
|
||||
<BudgetSummary agents={companyAgents} costs={companyCosts} />
|
||||
</Panel>
|
||||
}
|
||||
/>
|
||||
) : costsTab === "policies" ? (
|
||||
<PolicyList
|
||||
companyId={currentCompany!.id}
|
||||
agents={companyAgents.map((a) => ({ id: a.id, name: a.name }))}
|
||||
projects={companyProjects.map((p) => ({ id: p.id, name: p.name }))}
|
||||
/>
|
||||
) : (
|
||||
<IncidentList
|
||||
companyId={currentCompany!.id}
|
||||
agents={companyAgents.map((a) => ({ id: a.id, name: a.name }))}
|
||||
projects={companyProjects.map((p) => ({ id: p.id, name: p.name }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{section === "activity" ? (
|
||||
|
||||
@@ -13,3 +13,6 @@ export { WorkspaceCreator } from "./execution/WorkspaceCreator"
|
||||
export { SecretCreator } from "./execution/SecretCreator"
|
||||
export { SettingsPane } from "./execution/SettingsPane"
|
||||
export { PendingReviewList } from "./execution/PendingReviewList"
|
||||
export { PolicyList } from "./execution/PolicyList"
|
||||
export { PolicyFormModal } from "./execution/PolicyFormModal"
|
||||
export { IncidentList } from "./execution/IncidentList"
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { BudgetPolicyRecord } from "@shared/types";
|
||||
import { ActionButton, Input, Modal, Select } from "../ui";
|
||||
|
||||
interface PolicyFormModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
companyId: string;
|
||||
editingPolicy: BudgetPolicyRecord | null;
|
||||
agents: Array<{ id: string; name: string }>;
|
||||
projects: Array<{ id: string; name: string }>;
|
||||
}
|
||||
|
||||
export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPolicy, agents, projects }: PolicyFormModalProps) {
|
||||
const [scopeType, setScopeType] = useState<"company" | "agent" | "project">("company");
|
||||
const [scopeId, setScopeId] = useState(companyId);
|
||||
const [amountDollars, setAmountDollars] = useState("100");
|
||||
const [warnPercent, setWarnPercent] = useState("80");
|
||||
const [hardStopEnabled, setHardStopEnabled] = useState(true);
|
||||
const [notifyEnabled, setNotifyEnabled] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (editingPolicy) {
|
||||
setScopeType(editingPolicy.scopeType);
|
||||
setScopeId(editingPolicy.scopeId);
|
||||
setAmountDollars((editingPolicy.amount / 100).toFixed(2));
|
||||
setWarnPercent(String(editingPolicy.warnPercent));
|
||||
setHardStopEnabled(editingPolicy.hardStopEnabled);
|
||||
setNotifyEnabled(editingPolicy.notifyEnabled);
|
||||
} else {
|
||||
setScopeType("company");
|
||||
setScopeId(companyId);
|
||||
setAmountDollars("100");
|
||||
setWarnPercent("80");
|
||||
setHardStopEnabled(true);
|
||||
setNotifyEnabled(true);
|
||||
}
|
||||
}, [editingPolicy, companyId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scopeType === "company") {
|
||||
setScopeId(companyId);
|
||||
} else if (scopeType === "agent" && agents.length > 0) {
|
||||
setScopeId(agents[0].id);
|
||||
} else if (scopeType === "project" && projects.length > 0) {
|
||||
setScopeId(projects[0].id);
|
||||
}
|
||||
}, [scopeType, companyId, agents, projects]);
|
||||
|
||||
async function handleSave() {
|
||||
setSaving(true);
|
||||
try {
|
||||
const amountCents = Math.round(parseFloat(amountDollars) * 100);
|
||||
if (Number.isNaN(amountCents) || amountCents <= 0) return;
|
||||
|
||||
const wp = parseInt(warnPercent, 10);
|
||||
if (Number.isNaN(wp) || wp < 1 || wp > 100) return;
|
||||
|
||||
const result = await window.agentCompany.saveBudgetPolicy({
|
||||
companyId,
|
||||
scopeType,
|
||||
scopeId,
|
||||
metric: "billed_cents",
|
||||
windowKind: "calendar_month_utc",
|
||||
amount: amountCents,
|
||||
warnPercent: wp,
|
||||
hardStopEnabled,
|
||||
notifyEnabled,
|
||||
isActive: true,
|
||||
});
|
||||
if (result.ok) {
|
||||
onSaved();
|
||||
onClose();
|
||||
}
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={editingPolicy ? "Edit Budget Policy" : "Add Budget Policy"}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
<Select
|
||||
label="Scope"
|
||||
value={scopeType}
|
||||
onChange={(v) => setScopeType(v as "company" | "agent" | "project")}
|
||||
options={[
|
||||
{ value: "company", label: "Company" },
|
||||
{ value: "agent", label: "Agent" },
|
||||
{ value: "project", label: "Project" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{scopeType === "agent" ? (
|
||||
<Select
|
||||
label="Agent"
|
||||
value={scopeId}
|
||||
onChange={setScopeId}
|
||||
options={agents.map((a) => ({ value: a.id, label: a.name }))}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{scopeType === "project" ? (
|
||||
<Select
|
||||
label="Project"
|
||||
value={scopeId}
|
||||
onChange={setScopeId}
|
||||
options={projects.map((p) => ({ value: p.id, label: p.name }))}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Input
|
||||
label="Monthly Budget (USD)"
|
||||
value={amountDollars}
|
||||
onChange={setAmountDollars}
|
||||
type="number"
|
||||
placeholder="100.00"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Warning Threshold (%)"
|
||||
value={warnPercent}
|
||||
onChange={setWarnPercent}
|
||||
type="number"
|
||||
placeholder="80"
|
||||
/>
|
||||
|
||||
<label className="flex items-center gap-2.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hardStopEnabled}
|
||||
onChange={(e) => setHardStopEnabled(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
|
||||
/>
|
||||
<span className="text-[13px] text-[color:var(--text)]">Enable hard stop (block new runs at 100%)</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notifyEnabled}
|
||||
onChange={(e) => setNotifyEnabled(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
|
||||
/>
|
||||
<span className="text-[13px] text-[color:var(--text)]">Send notifications</span>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-[8px] px-4 py-2 text-[13px] font-medium text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<ActionButton
|
||||
label={saving ? "Saving..." : "Save Policy"}
|
||||
tone="accent"
|
||||
disabled={saving}
|
||||
onClick={() => void handleSave()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Plus, ShieldCheck, Trash, Warning } from "@phosphor-icons/react";
|
||||
import type { BudgetPolicyRecord, BudgetStatusSummary } from "@shared/types";
|
||||
import { ActionButton, EmptyState, ListFrame, ListRow } from "../ui";
|
||||
import { PolicyFormModal } from "./PolicyFormModal";
|
||||
|
||||
interface PolicyListProps {
|
||||
companyId: string;
|
||||
agents: Array<{ id: string; name: string }>;
|
||||
projects: Array<{ id: string; name: string }>;
|
||||
}
|
||||
|
||||
export function PolicyList({ companyId, agents, projects }: PolicyListProps) {
|
||||
const [policies, setPolicies] = useState<BudgetPolicyRecord[]>([]);
|
||||
const [budgetStatus, setBudgetStatus] = useState<BudgetStatusSummary | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingPolicy, setEditingPolicy] = useState<BudgetPolicyRecord | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
const [polRes, statusRes] = await Promise.all([
|
||||
window.agentCompany.listBudgetPolicies(companyId),
|
||||
window.agentCompany.getBudgetStatus(companyId),
|
||||
]);
|
||||
if (polRes.ok) setPolicies(polRes.data);
|
||||
if (statusRes.ok) setBudgetStatus(statusRes.data);
|
||||
}, [companyId]);
|
||||
|
||||
useEffect(() => { void loadData(); }, [loadData]);
|
||||
|
||||
function getScopeName(policy: BudgetPolicyRecord): string {
|
||||
if (policy.scopeType === "company") return "Company";
|
||||
if (policy.scopeType === "agent") {
|
||||
const agent = agents.find((a) => a.id === policy.scopeId);
|
||||
return agent ? agent.name : policy.scopeId.slice(0, 8);
|
||||
}
|
||||
if (policy.scopeType === "project") {
|
||||
const project = projects.find((p) => p.id === policy.scopeId);
|
||||
return project ? project.name : policy.scopeId.slice(0, 8);
|
||||
}
|
||||
return policy.scopeId.slice(0, 8);
|
||||
}
|
||||
|
||||
function getUtilization(policyId: string): { spent: number; amount: number; pct: number; hasIncident: boolean } | null {
|
||||
if (!budgetStatus) return null;
|
||||
const entry = budgetStatus.policies.find((p) => p.policyId === policyId);
|
||||
if (!entry) return null;
|
||||
const pct = entry.amount > 0 ? (entry.spent / entry.amount) * 100 : 0;
|
||||
return { spent: entry.spent, amount: entry.amount, pct, hasIncident: entry.hasOpenIncident };
|
||||
}
|
||||
|
||||
async function handleDelete(policyId: string) {
|
||||
const result = await window.agentCompany.deleteBudgetPolicy({ companyId, policyId });
|
||||
if (result.ok) void loadData();
|
||||
}
|
||||
|
||||
function handleEdit(policy: BudgetPolicyRecord) {
|
||||
setEditingPolicy(policy);
|
||||
setShowForm(true);
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
setEditingPolicy(null);
|
||||
setShowForm(true);
|
||||
}
|
||||
|
||||
function handleSaved() {
|
||||
void loadData();
|
||||
}
|
||||
|
||||
if (policies.length === 0 && !showForm) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<EmptyState
|
||||
title="No budget policies"
|
||||
detail="Add your first policy to track and enforce spending limits."
|
||||
action={<ActionButton label="Add Policy" icon={Plus} onClick={handleAdd} />}
|
||||
/>
|
||||
<PolicyFormModal
|
||||
open={showForm}
|
||||
onClose={() => setShowForm(false)}
|
||||
onSaved={handleSaved}
|
||||
companyId={companyId}
|
||||
editingPolicy={editingPolicy}
|
||||
agents={agents}
|
||||
projects={projects}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 Policies</h3>
|
||||
<ActionButton label="Add Policy" icon={Plus} onClick={handleAdd} />
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-4 pb-4">
|
||||
<ListFrame>
|
||||
{policies.map((policy) => {
|
||||
const util = getUtilization(policy.id);
|
||||
const pct = util?.pct ?? 0;
|
||||
const barColor = pct > 100 ? "bg-[color:var(--danger)]" : pct > 80 ? "bg-[color:var(--warn)]" : "bg-[color:var(--success)]";
|
||||
return (
|
||||
<ListRow key={policy.id} onClick={() => handleEdit(policy)}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center rounded-full bg-[color:var(--panel-soft)] px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] text-[color:var(--muted-strong)]">
|
||||
{policy.scopeType}
|
||||
</span>
|
||||
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{getScopeName(policy)}</span>
|
||||
{policy.hardStopEnabled ? <ShieldCheck size={14} className="shrink-0 text-[color:var(--accent)]" /> : null}
|
||||
{util?.hasIncident ? <Warning size={14} weight="fill" className="shrink-0 text-[color:var(--warn)]" /> : null}
|
||||
</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" }}>
|
||||
${(policy.amount / 100).toFixed(2)}/mo
|
||||
</span>
|
||||
<span>Warn at {policy.warnPercent}%</span>
|
||||
</div>
|
||||
{util ? (
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<div className="h-1.5 flex-1 rounded-full bg-[color:var(--line)]">
|
||||
<div className={`h-1.5 rounded-full ${barColor}`} style={{ width: `${Math.min(100, pct)}%` }} />
|
||||
</div>
|
||||
<span className="text-[10px] font-[family-name:var(--font-mono)] text-[color:var(--muted)]" style={{ fontVariantNumeric: "tabular-nums" }}>
|
||||
{Math.round(pct)}%
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 rounded p-1 text-[color:var(--muted)] transition hover:text-[color:var(--danger)]"
|
||||
title="Delete policy"
|
||||
aria-label="Delete policy"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void handleDelete(policy.id);
|
||||
}}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</ListRow>
|
||||
);
|
||||
})}
|
||||
</ListFrame>
|
||||
</div>
|
||||
<PolicyFormModal
|
||||
open={showForm}
|
||||
onClose={() => { setShowForm(false); setEditingPolicy(null); }}
|
||||
onSaved={handleSaved}
|
||||
companyId={companyId}
|
||||
editingPolicy={editingPolicy}
|
||||
agents={agents}
|
||||
projects={projects}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user