mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 11:20:37 +09:00
feat(costs): add billing, quota, and budget control plane
This commit is contained in:
parent
656b4659fc
commit
76e6cc08a6
91 changed files with 22406 additions and 769 deletions
71
ui/src/components/FinanceTimelineCard.tsx
Normal file
71
ui/src/components/FinanceTimelineCard.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import type { FinanceEvent } from "@paperclipai/shared";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
financeDirectionDisplayName,
|
||||
financeEventKindDisplayName,
|
||||
formatCents,
|
||||
formatDateTime,
|
||||
providerDisplayName,
|
||||
} from "@/lib/utils";
|
||||
|
||||
interface FinanceTimelineCardProps {
|
||||
rows: FinanceEvent[];
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
export function FinanceTimelineCard({
|
||||
rows,
|
||||
emptyMessage = "No financial events in this period.",
|
||||
}: FinanceTimelineCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="px-4 pt-4 pb-1">
|
||||
<CardTitle className="text-base">Recent financial events</CardTitle>
|
||||
<CardDescription>Top-ups, fees, credits, commitments, and other non-request charges.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 px-4 pb-4 pt-3">
|
||||
{rows.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">{emptyMessage}</p>
|
||||
) : (
|
||||
rows.map((row) => (
|
||||
<div
|
||||
key={row.id}
|
||||
className="border border-border p-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="secondary">{financeEventKindDisplayName(row.eventKind)}</Badge>
|
||||
<Badge variant={row.direction === "credit" ? "outline" : "secondary"}>
|
||||
{financeDirectionDisplayName(row.direction)}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">{formatDateTime(row.occurredAt)}</span>
|
||||
</div>
|
||||
<div className="text-sm font-medium">
|
||||
{providerDisplayName(row.biller)}
|
||||
{row.provider ? ` -> ${providerDisplayName(row.provider)}` : ""}
|
||||
{row.model ? <span className="ml-1 font-mono text-xs text-muted-foreground">{row.model}</span> : null}
|
||||
</div>
|
||||
{(row.description || row.externalInvoiceId || row.region || row.pricingTier) && (
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
{row.description ? <div>{row.description}</div> : null}
|
||||
{row.externalInvoiceId ? <div>invoice {row.externalInvoiceId}</div> : null}
|
||||
{row.region ? <div>region {row.region}</div> : null}
|
||||
{row.pricingTier ? <div>tier {row.pricingTier}</div> : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right tabular-nums">
|
||||
<div className="text-sm font-semibold">{formatCents(row.amountCents)}</div>
|
||||
<div className="text-xs text-muted-foreground">{row.currency}</div>
|
||||
{row.estimated ? <div className="text-[11px] uppercase tracking-[0.12em] text-amber-600">estimated</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue