Build the agent-native compute CRM platform
CI / verify (push) Successful in 3m6s

This commit is contained in:
2026-08-13 01:39:01 -07:00
parent bfd2f8d95a
commit 853bde2265
160 changed files with 61812 additions and 483 deletions
+43 -33
View File
@@ -8,30 +8,18 @@
*/
import { useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { PermissionGrant } from '@pig/core';
import { Pencil, Plus } from 'lucide-react';
import { get, money, relativeTime } from '@/lib/api';
import { Badge, Card, EmptyState, Skeleton } from '@/components/ui';
import { Badge, Button, Card, EmptyState, Skeleton } from '@/components/ui';
import { usePageTitle } from '@/lib/title';
interface DemandDeal {
id: string;
name: string;
stage: string;
productLine: string;
acvCents: number | null;
msaExecuted: boolean;
dpaExecuted: boolean;
updatedAt: string;
}
interface SupplyDeal {
id: string;
name: string;
stage: string;
gpuType: string | null;
gpuCount: number | null;
targetCostPerGpuHourCents: number | null;
updatedAt: string;
}
import { can } from '@/lib/permissions';
import {
DemandDealSheet,
SupplyDealSheet,
type DemandDealRecord,
type SupplyDealRecord,
} from '@/components/RecordSheets';
interface Board<T> {
stages: string[];
@@ -64,10 +52,12 @@ const STAGE_LABELS: Record<string, string> = {
export function DemandPipeline() {
return (
<PipelineBoard<DemandDeal>
<PipelineBoard<DemandDealRecord>
title="Demand"
subtitle="Selling compute and post-training. Note that legal sits early — paper gates the deal rather than closing it."
endpoint="/api/deals/demand"
team="demand"
renderSheet={({ open, onOpenChange, record }) => <DemandDealSheet open={open} onOpenChange={onOpenChange} record={record} />}
renderCard={(deal, accountName) => (
<>
<p className="truncate font-medium">{deal.name}</p>
@@ -90,10 +80,12 @@ export function DemandPipeline() {
export function SupplyPipeline() {
return (
<PipelineBoard<SupplyDeal>
<PipelineBoard<SupplyDealRecord>
title="Supply"
subtitle="Sourcing GPU capacity. Technical and financial diligence are separate gates — accepting capacity is a two-key decision."
endpoint="/api/deals/supply"
team="supply"
renderSheet={({ open, onOpenChange, record }) => <SupplyDealSheet open={open} onOpenChange={onOpenChange} record={record} />}
renderCard={(deal, accountName) => (
<>
<p className="truncate font-medium">{deal.name}</p>
@@ -120,12 +112,16 @@ function PipelineBoard<T extends { id: string; stage: string; updatedAt: string
title,
subtitle,
endpoint,
team,
renderCard,
renderSheet,
}: {
title: string;
subtitle: string;
endpoint: string;
team: 'supply' | 'demand';
renderCard: (deal: T, accountName: string | null) => React.ReactNode;
renderSheet: (props: { open: boolean; onOpenChange(open: boolean): void; record?: T }) => React.ReactNode;
}) {
usePageTitle(title);
@@ -133,6 +129,12 @@ function PipelineBoard<T extends { id: string; stage: string; updatedAt: string
queryKey: [endpoint],
queryFn: () => get<Board<T>>(endpoint),
});
const { data: me } = useQuery({
queryKey: ['me'],
queryFn: () => get<{ permissions: PermissionGrant[] }>('/api/me'),
});
const writable = can(me, 'deal:write', team);
const [sheet, setSheet] = useState<{ open: boolean; record?: T }>({ open: false });
const [activeStage, setActiveStage] = useState<string | null>(null);
@@ -150,13 +152,15 @@ function PipelineBoard<T extends { id: string; stage: string; updatedAt: string
if (!data || data.deals.length === 0) {
return (
<div className="space-y-5">
<Header title={title} subtitle={subtitle} />
<Header title={title} subtitle={subtitle} writable={writable} onCreate={() => setSheet({ open: true })} />
<Card>
<EmptyState
title={`No ${title.toLowerCase()} deals yet`}
description="Deals appear here once created. Stages follow how this market actually operates rather than a generic sales funnel."
action={<Button variant="primary" disabled={!writable} onClick={() => setSheet({ open: true })}><Plus aria-hidden />New {title.toLowerCase()} deal</Button>}
/>
</Card>
{renderSheet({ open: sheet.open, onOpenChange: (open) => setSheet((state) => ({ ...state, open })), record: sheet.record })}
</div>
);
}
@@ -166,7 +170,7 @@ function PipelineBoard<T extends { id: string; stage: string; updatedAt: string
return (
<div className="space-y-5">
<Header title={title} subtitle={subtitle} />
<Header title={title} subtitle={subtitle} writable={writable} onCreate={() => setSheet({ open: true })} />
{/* Phone: pick one stage. The chips scroll; the board does not. */}
<div className="lg:hidden">
@@ -192,7 +196,7 @@ function PipelineBoard<T extends { id: string; stage: string; updatedAt: string
</div>
<div className="mt-3 space-y-2">
{(byStage.get(currentStage) ?? []).map((row) => (
<DealCard key={row.deal.id} row={row} renderCard={renderCard} />
<DealCard key={row.deal.id} row={row} renderCard={renderCard} writable={writable} onEdit={() => setSheet({ open: true, record: row.deal })} />
))}
{(byStage.get(currentStage) ?? []).length === 0 ? (
<p className="py-8 text-center text-sm text-muted">
@@ -216,7 +220,7 @@ function PipelineBoard<T extends { id: string; stage: string; updatedAt: string
</div>
<div className="space-y-2">
{rows.map((row) => (
<DealCard key={row.deal.id} row={row} renderCard={renderCard} />
<DealCard key={row.deal.id} row={row} renderCard={renderCard} writable={writable} onEdit={() => setSheet({ open: true, record: row.deal })} />
))}
</div>
</section>
@@ -224,6 +228,7 @@ function PipelineBoard<T extends { id: string; stage: string; updatedAt: string
})}
</div>
</div>
{renderSheet({ open: sheet.open, onOpenChange: (open) => setSheet((state) => ({ ...state, open })), record: sheet.record })}
</div>
);
}
@@ -231,23 +236,28 @@ function PipelineBoard<T extends { id: string; stage: string; updatedAt: string
function DealCard<T extends { id: string; updatedAt: string }>({
row,
renderCard,
writable,
onEdit,
}: {
row: { deal: T; accountName: string | null };
renderCard: (deal: T, accountName: string | null) => React.ReactNode;
writable: boolean;
onEdit(): void;
}) {
return (
<article className="card p-3">
<article className="card relative p-3 pr-12">
<Button size="icon" variant="ghost" className="absolute right-1 top-1" disabled={!writable} onClick={onEdit} title="Edit deal"><Pencil aria-hidden /><span className="sr-only">Edit deal</span></Button>
{renderCard(row.deal, row.accountName)}
<p className="mt-2 text-[11px] text-muted">{relativeTime(row.deal.updatedAt)}</p>
</article>
);
}
function Header({ title, subtitle }: { title: string; subtitle: string }) {
function Header({ title, subtitle, writable, onCreate }: { title: string; subtitle: string; writable: boolean; onCreate(): void }) {
return (
<header>
<h1 className="text-xl font-semibold tracking-tight sm:text-2xl">{title}</h1>
<p className="mt-1 max-w-2xl text-sm text-muted">{subtitle}</p>
<header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div><h1 className="text-xl font-semibold tracking-tight sm:text-2xl">{title}</h1><p className="mt-1 max-w-2xl text-sm text-muted">{subtitle}</p></div>
<Button variant="primary" disabled={!writable} onClick={onCreate}><Plus aria-hidden />New deal</Button>
</header>
);
}