feat: add ProformaStatusSegmentedFilter component and integrate into list-proformas-page

This commit is contained in:
David Arranz 2026-07-30 11:19:16 +02:00
parent 1bf85a05ff
commit fed29a71fd
6 changed files with 154 additions and 82 deletions

View File

@ -1,3 +1,4 @@
export * from "./archive-proforma-dialog"; export * from "./archive-proforma-dialog";
export * from "./proforma-status-badge"; export * from "./proforma-status-badge";
export * from "./unarchive-proforma-dialog"; export * from "./unarchive-proforma-dialog";
export * from "./proforma-status-segmented-filter";

View File

@ -0,0 +1,77 @@
import { ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { useTranslation } from "../../../../i18n";
import type { ProformaListStatusFilter } from "../../types/proforma-list-filters";
export type ProformaStatusFilter = ProformaListStatusFilter;
export type ProformaStatusSegmentedFilterProps = {
value: ProformaStatusFilter;
onValueChange: (value: ProformaStatusFilter) => void;
disabled?: boolean;
};
const PROFORMA_STATUS_FILTER_OPTIONS = [
{ value: "all", labelKey: "catalog.proformas.status.all.label" },
{ value: "draft", labelKey: "catalog.proformas.status.draft.label" },
{ value: "sent", labelKey: "catalog.proformas.status.sent.label" },
{ value: "approved", labelKey: "catalog.proformas.status.approved.label" },
{ value: "rejected", labelKey: "catalog.proformas.status.rejected.label" },
{ value: "issued", labelKey: "catalog.proformas.status.issued.label" },
] as const satisfies readonly {
value: ProformaStatusFilter;
labelKey: string;
}[];
const isProformaStatusFilter = (value: string): value is ProformaStatusFilter => {
return PROFORMA_STATUS_FILTER_OPTIONS.some((option) => option.value === value);
};
export const ProformaStatusSegmentedFilter = ({
value,
onValueChange,
disabled = false,
}: ProformaStatusSegmentedFilterProps) => {
const { t } = useTranslation();
const handleValueChange = (nextValue: string) => {
if (!(nextValue && isProformaStatusFilter(nextValue)) || nextValue === value) {
return;
}
onValueChange(nextValue);
};
return (
<ToggleGroup
aria-label={t("pages.proformas.list.filters.status.label")}
className="w-full flex-wrap justify-start"
disabled={disabled}
onValueChange={handleValueChange}
size="sm"
spacing={0}
type="single"
value={value}
variant="outline"
>
{PROFORMA_STATUS_FILTER_OPTIONS.map((option) => (
<ToggleGroupItem
className={cn(
"min-w-fit px-3 text-xs sm:text-sm",
"data-[state=on]:border-primary data-[state=on]:bg-primary data-[state=on]:text-primary-foreground data-[state=on]:shadow-none"
)}
key={option.value}
onPointerDown={(event) => {
if (option.value === value) {
event.preventDefault();
}
}}
value={option.value}
>
{t(option.labelKey)}
</ToggleGroupItem>
))}
</ToggleGroup>
);
};

View File

@ -8,18 +8,12 @@ import {
ResizableHandle, ResizableHandle,
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import { import {
CheckCircle2Icon, CheckCircle2Icon,
CircleDollarSignIcon, CircleDollarSignIcon,
ClockIcon, ClockIcon,
FileTextIcon, FileTextIcon,
FilterIcon,
PlusIcon, PlusIcon,
} from "lucide-react"; } from "lucide-react";
import { createSearchParams, useNavigate } from "react-router-dom"; import { createSearchParams, useNavigate } from "react-router-dom";
@ -32,10 +26,15 @@ import { prepareDeleteProformaTargets } from "../../../delete/utils";
import { IssueProformaDialog } from "../../../issue-proforma"; import { IssueProformaDialog } from "../../../issue-proforma";
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils"; import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
import type { ProformaListRow } from "../../../shared"; import type { ProformaListRow } from "../../../shared";
import type { ProformaListScope } from "../../types/proforma-list-filters";
import { useListProformasPageController } from "../../controllers"; import { useListProformasPageController } from "../../controllers";
import type { ProformaListScope } from "../../types/proforma-list-filters";
import { ProformaSummaryPanel, ProformasGrid, useProformasGridColumns } from "../blocks"; import { ProformaSummaryPanel, ProformasGrid, useProformasGridColumns } from "../blocks";
import { ArchiveProformaDialog, ProformaStatusBadge, UnarchiveProformaDialog } from "../components"; import {
ArchiveProformaDialog,
ProformaStatusBadge,
ProformaStatusSegmentedFilter,
UnarchiveProformaDialog,
} from "../components";
interface ScopedListProformasPageProps { interface ScopedListProformasPageProps {
scope: ProformaListScope; scope: ProformaListScope;
@ -136,23 +135,11 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
value={listCtrl.search} value={listCtrl.search}
/> />
<Select <ProformaStatusSegmentedFilter
onValueChange={(value) => listCtrl.setStatusFilter(value ?? "all")} disabled={listCtrl.isLoading}
onValueChange={listCtrl.setStatusFilter}
value={listCtrl.statusFilter} value={listCtrl.statusFilter}
> />
<SelectTrigger className="w-full sm:w-48">
<FilterIcon aria-hidden className="mr-2 size-4" />
<SelectValue placeholder={t("pages.proformas.list.filters.status.label")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t("catalog.proformas.status.all.label")}</SelectItem>
<SelectItem value="draft">{t("catalog.proformas.status.draft.label")}</SelectItem>
<SelectItem value="sent">{t("catalog.proformas.status.sent.label")}</SelectItem>
<SelectItem value="approved">{t("catalog.proformas.status.approved.label")}</SelectItem>
<SelectItem value="rejected">{t("catalog.proformas.status.rejected.label")}</SelectItem>
<SelectItem value="issued">{t("catalog.proformas.status.issued.label")}</SelectItem>
</SelectContent>
</Select>
</div> </div>
<div> <div>
<ProformasGrid <ProformasGrid

View File

@ -39,6 +39,7 @@
"typescript": "^6.0.2" "typescript": "^6.0.2"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-toggle-group": "^1.1.11",
"@base-ui/react": "^1.3.0", "@base-ui/react": "^1.3.0",
"@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/geist": "^5.2.8",
"@fontsource-variable/geist-mono": "^5.2.7", "@fontsource-variable/geist-mono": "^5.2.7",

View File

@ -1,87 +1,90 @@
import * as React from "react" import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle" import { toggleVariants } from "@repo/shadcn-ui/components/toggle";
import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group" import { cn } from "@repo/shadcn-ui/lib/utils";
import type { VariantProps } from "class-variance-authority" import type { VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@repo/shadcn-ui/lib/utils"
import { toggleVariants } from "@repo/shadcn-ui/components/toggle"
const ToggleGroupContext = React.createContext< const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & { VariantProps<typeof toggleVariants> & {
spacing?: number spacing?: number;
orientation?: "horizontal" | "vertical" orientation?: "horizontal" | "vertical";
} }
>({ >({
size: "default", size: "default",
variant: "default", variant: "default",
spacing: 0, spacing: 0,
orientation: "horizontal", orientation: "horizontal",
}) });
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants> & {
spacing?: number;
}
>(({ className, variant, size, spacing = 0, children, style, ...props }, ref) => {
const orientation = props.orientation ?? "horizontal";
function ToggleGroup({
className,
variant,
size,
spacing = 0,
orientation = "horizontal",
children,
...props
}: ToggleGroupPrimitive.Props &
VariantProps<typeof toggleVariants> & {
spacing?: number
orientation?: "horizontal" | "vertical"
}) {
return ( return (
<ToggleGroupPrimitive <ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
data-orientation={orientation}
style={{ "--gap": spacing } as React.CSSProperties}
className={cn( className={cn(
"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=0]:data-[variant=outline]:shadow-xs data-vertical:flex-col data-vertical:items-stretch", "group/toggle-group flex w-fit rounded-md",
orientation === "vertical" ? "flex-col items-stretch" : "flex-row items-center",
className className
)} )}
data-orientation={orientation}
data-size={size}
data-slot="toggle-group"
data-spacing={spacing}
data-variant={variant}
ref={ref}
style={{
gap: spacing === 0 ? undefined : `${spacing * 0.25}rem`,
...style,
}}
{...props} {...props}
> >
<ToggleGroupContext.Provider <ToggleGroupContext.Provider value={{ variant, size, spacing, orientation }}>
value={{ variant, size, spacing, orientation }}
>
{children} {children}
</ToggleGroupContext.Provider> </ToggleGroupContext.Provider>
</ToggleGroupPrimitive> </ToggleGroupPrimitive.Root>
) );
} });
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
function ToggleGroupItem({ const ToggleGroupItem = React.forwardRef<
className, React.ElementRef<typeof ToggleGroupPrimitive.Item>,
children, React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
variant = "default", VariantProps<typeof toggleVariants>
size = "default", >(({ className, children, variant, size, ...props }, ref) => {
...props const context = React.useContext(ToggleGroupContext);
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
return ( return (
<TogglePrimitive <ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
data-spacing={context.spacing}
className={cn( className={cn(
"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t", "shrink-0 rounded-md focus:z-10 focus-visible:z-10 data-[state=on]:bg-muted data-[state=on]:text-foreground",
"group-data-[spacing=0]/toggle-group:first:rounded-r-none group-data-[spacing=0]/toggle-group:last:rounded-l-none group-data-[spacing=0]/toggle-group:not-first:not-last:rounded-none",
"group-data-[spacing=0]/toggle-group:data-[orientation=vertical]:first:rounded-b-none group-data-[spacing=0]/toggle-group:data-[orientation=vertical]:last:rounded-t-none",
"group-data-[spacing=0]/toggle-group:data-[orientation=vertical]:not-first:not-last:rounded-none",
"group-data-[spacing=0]/toggle-group:not-first:-ml-px group-data-[spacing=0]/toggle-group:data-[orientation=vertical]:not-first:ml-0 group-data-[spacing=0]/toggle-group:data-[orientation=vertical]:not-first:-mt-px",
toggleVariants({ toggleVariants({
variant: context.variant || variant, variant: context.variant ?? variant ?? "default",
size: context.size || size, size: context.size ?? size ?? "default",
}), }),
className className
)} )}
data-orientation={context.orientation}
data-size={context.size || size}
data-slot="toggle-group-item"
data-spacing={context.spacing}
data-variant={context.variant || variant}
ref={ref}
{...props} {...props}
> >
{children} {children}
</TogglePrimitive> </ToggleGroupPrimitive.Item>
) );
} });
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem } export { ToggleGroup, ToggleGroupItem };

View File

@ -1383,6 +1383,9 @@ importers:
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.2.2 specifier: ^5.2.2
version: 5.4.0(react-hook-form@7.76.1(react@19.2.6)) version: 5.4.0(react-hook-form@7.76.1(react@19.2.6))
'@radix-ui/react-toggle-group':
specifier: ^1.1.11
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
add: add:
specifier: ^2.0.6 specifier: ^2.0.6
version: 2.0.6 version: 2.0.6