p2p/src/components/CommandPalette.tsx
Bhanu Prakash Sai Potteri f5bc1fa119 feat: rebuild the UI on Tailwind v4 + shadcn/ui
Migrates Tailwind v3 -> v4 (CSS-first, theme tokens for brand, workflow
states and charts) and adds 23 shadcn/ui components on Radix.

- lib/workflow.ts is now the single source of truth for state name, colour,
  icon and available actions; "Financial Review" is the canonical served
  name, with "Finance Review" kept as an alias so those rows stop falling
  through to the unstyled fallback and dropping out of every count
- record view: TanStack Table with server-side sort/paginate, company column
  with initials avatar, page totals, density and a URL-synced query state
- dashboard: one cached analytics fetch shared by tiles and charts, donut +
  aging + vendor spend, click-to-filter
- detail view: rebuilt against the live detailview payload — restores the PO
  number and payment terms that the old key heuristics silently dropped,
  parses match_summary into a 3-way-match checklist, and separates the
  department's and reviewer's own words from AI-generated content
- form modal on Radix Dialog (focus trap, scroll lock, escape handling)
2026-07-29 19:47:59 +05:30

154 lines
4.9 KiB
TypeScript

import { useEffect } from "react";
import { useQueryStates } from "nuqs";
import { FileText, ListFilter, Moon, Search, Sun, X } from "lucide-react";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import { useTheme } from "@/hooks/ThemeContext";
import { useHeaderConfig } from "@/hooks/useHeaderConfig";
import { tableParams, tableParamsOptions } from "@/lib/tableParams";
import { WORKFLOW_STATES } from "@/lib/workflow";
import { cn } from "@/lib/utils";
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
onNavigate: (screenUuid: string) => void;
}
/**
* ⌘K palette — jump between screens and apply status filters without leaving
* the keyboard. This is a queue-triage app; reaching for the mouse to change a
* filter is the slow path.
*/
export default function CommandPalette({ open, onOpenChange, onNavigate }: Props) {
const { theme, toggleTheme } = useTheme();
const { navItems } = useHeaderConfig();
const [params, setParams] = useQueryStates(tableParams, tableParamsOptions);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
onOpenChange(!open);
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onOpenChange]);
const run = (fn: () => void) => {
fn();
onOpenChange(false);
};
const filtersActive = params.status.length > 0 || !!params.q;
return (
<CommandDialog
open={open}
onOpenChange={onOpenChange}
title="Command palette"
description="Jump to a screen or filter the queue"
>
<CommandInput placeholder="Jump to a screen, or filter by status…" />
<CommandList>
<CommandEmpty>No matches.</CommandEmpty>
{navItems.length > 0 && (
<CommandGroup heading="Screens">
{navItems.map((item) => (
<CommandItem
key={item.id}
value={`screen ${item.label}`}
onSelect={() => run(() => onNavigate(item.screen_uuid))}
>
<FileText className="size-3.5" aria-hidden />
{item.label}
</CommandItem>
))}
</CommandGroup>
)}
<CommandSeparator />
<CommandGroup heading="Filter by status">
{WORKFLOW_STATES.map((s) => {
const selected = params.status.includes(s.name);
return (
<CommandItem
key={s.key}
value={`filter ${s.label}`}
onSelect={() =>
run(() =>
setParams({
status: selected
? params.status.filter((x) => x !== s.name)
: [...params.status, s.name],
page: 1,
}),
)
}
>
<span className={cn("size-2 shrink-0 rounded-full", s.tone.solid)} aria-hidden />
{s.label}
{selected && <CommandShortcut>active</CommandShortcut>}
</CommandItem>
);
})}
{filtersActive && (
<CommandItem
value="clear filters"
onSelect={() => run(() => setParams({ status: [], q: "", page: 1 }))}
>
<X className="size-3.5" aria-hidden />
Clear all filters
</CommandItem>
)}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Actions">
<CommandItem
value="focus search"
onSelect={() =>
run(() => {
// Defer so the dialog has released focus first.
requestAnimationFrame(() => {
document
.querySelector<HTMLInputElement>('input[aria-label="Search records"]')
?.focus();
});
})
}
>
<Search className="size-3.5" aria-hidden />
Search records
<CommandShortcut>/</CommandShortcut>
</CommandItem>
<CommandItem value="toggle theme" onSelect={() => run(toggleTheme)}>
{theme === "dark" ? (
<Sun className="size-3.5" aria-hidden />
) : (
<Moon className="size-3.5" aria-hidden />
)}
Switch to {theme === "dark" ? "light" : "dark"} mode
</CommandItem>
<CommandItem value="filters help" disabled>
<ListFilter className="size-3.5" aria-hidden />
Tip: tiles on the dashboard also filter the queue
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
);
}