import Link from "next/link"; import { CalendarDaysIcon, KanbanIcon, ListIcon } from "lucide-react"; import type { Stage } from "@prisma/client"; import { STAGE_LABEL, TERMINAL_STAGES } from "@/lib/stages"; import { SearchBox } from "@/components/crm/search-box"; import { FilterMenu, type FilterFacets } from "@/components/pipeline/filter-menu"; import { buildPipelineQuery, type PipelineFilters } from "@/lib/pipeline-filters"; import { cn } from "@/lib/utils"; /** * What the Filter menu is holding, in words, beside the two chips. * * The stages left the toolbar; without a line like this, turning three of them * on shows a narrowed board whose only explanation is a number on a button. * Named up to three, counted after that, and the four endings collapse to * "Closed" because that is how they were picked. */ function stageSummary(stages: Stage[]) { const closed = TERMINAL_STAGES.every((stage) => stages.includes(stage)); const live = closed ? stages.filter((stage) => !TERMINAL_STAGES.includes(stage)) : stages; const parts = [...live.map((stage) => STAGE_LABEL[stage]), ...(closed ? ["Closed"] : [])]; if (parts.length <= 3) return parts.join(", "); return `${parts.slice(0, 2).join(", ")} and ${parts.length - 2} more`; } /** * The pipeline's controls, across the top. * * Horizontal rather than down the side because a board is the widest thing in * the product and a rail was taking 216px out of it for nine links. Across the * top the same controls cost two rows and the columns get their width back. * * Everything is a Link, so the whole thing is server-rendered and the URL is * the state — a view, a filter and a search you can paste to yourself. */ export const PIPELINE_VIEWS = ["board", "list", "calendar"] as const; export type PipelineView = (typeof PIPELINE_VIEWS)[number]; export function parseView(value: string | undefined): PipelineView { return (PIPELINE_VIEWS as readonly string[]).includes(value ?? "") ? (value as PipelineView) : "board"; } export type ToolbarCounts = { all: number; overdue: number; /** Per stage, for the Stage dimension inside the Filter menu. */ byStage: Record; }; /** * Counts come from the rows the page is holding, not from pipelineStats. * * The chips used to be fed by a separate aggregate that ignored `q`, so * searching "stripe" left "Screening 12" sitting above a board with one card * on it. With five more dimensions that lie would compound. */ const VIEWS: { view: PipelineView; label: string; icon: typeof ListIcon }[] = [ { view: "board", label: "Board", icon: KanbanIcon }, { view: "list", label: "List", icon: ListIcon }, { view: "calendar", label: "Calendar", icon: CalendarDaysIcon }, ]; export function PipelineToolbar({ view, filters, counts, facets, sort, dir, action, views, share, fields, exportLink, }: { view: PipelineView; filters: PipelineFilters; counts: ToolbarCounts; facets: FilterFacets; sort?: string; dir?: string; action: React.ReactNode; views: React.ReactNode; share: React.ReactNode; /** Which optional fields this view draws. */ fields: React.ReactNode; /** * Export what this screen is showing. Absent on the calendar, following the * `limited` precedent below: a calendar has no rows, so "export what's shown" * has no honest answer. */ exportLink?: React.ReactNode; }) { const href = (next: PipelineFilters, nextView: PipelineView = view) => buildPipelineQuery({ view: nextView, filters: next, sort, dir }); const nothingOn = filters.stages.length === 0 && !filters.overdue; return (
{VIEWS.map(({ view: candidate, label, icon: Icon }) => { const active = candidate === view; return ( {label} ); })}
{fields} {views} {share} {exportLink}
{action}
{/* Two chips, not twelve. The stages used to sit here as a row you scrolled sideways, above a board whose columns are the stages. They are a dimension like tags or companies and they live in the Filter menu now. What is left is the one cut worth a click from anywhere: what is overdue. The front page is built around it, the bell counts it, and burying it three clicks deep to tidy this row would have cost more than the row did. "Everything" stays beside it because a filter you cannot see the way out of is a trap. */}
Everything 0 ? "var(--destructive)" : undefined} emphasis={counts.overdue > 0} > Needs a nudge {filters.stages.length > 0 && ( {stageSummary(filters.stages)} )}
); } function Chip({ href, active, count, tone, emphasis, children, }: { href: string; active: boolean; count: number; tone?: string; emphasis?: boolean; children: React.ReactNode; }) { return ( {tone && } {children} {count || ""} ); }