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 "./proforma-status-badge";
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,
ResizablePanel,
ResizablePanelGroup,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/shadcn-ui/components";
import {
CheckCircle2Icon,
CircleDollarSignIcon,
ClockIcon,
FileTextIcon,
FilterIcon,
PlusIcon,
} from "lucide-react";
import { createSearchParams, useNavigate } from "react-router-dom";
@ -32,10 +26,15 @@ import { prepareDeleteProformaTargets } from "../../../delete/utils";
import { IssueProformaDialog } from "../../../issue-proforma";
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
import type { ProformaListRow } from "../../../shared";
import type { ProformaListScope } from "../../types/proforma-list-filters";
import { useListProformasPageController } from "../../controllers";
import type { ProformaListScope } from "../../types/proforma-list-filters";
import { ProformaSummaryPanel, ProformasGrid, useProformasGridColumns } from "../blocks";
import { ArchiveProformaDialog, ProformaStatusBadge, UnarchiveProformaDialog } from "../components";
import {
ArchiveProformaDialog,
ProformaStatusBadge,
ProformaStatusSegmentedFilter,
UnarchiveProformaDialog,
} from "../components";
interface ScopedListProformasPageProps {
scope: ProformaListScope;
@ -136,23 +135,11 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
value={listCtrl.search}
/>
<Select
onValueChange={(value) => listCtrl.setStatusFilter(value ?? "all")}
<ProformaStatusSegmentedFilter
disabled={listCtrl.isLoading}
onValueChange={listCtrl.setStatusFilter}
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>
<ProformasGrid

View File

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

View File

@ -1,87 +1,90 @@
import * as React from "react"
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group"
import type { VariantProps } from "class-variance-authority"
import { cn } from "@repo/shadcn-ui/lib/utils"
import { toggleVariants } from "@repo/shadcn-ui/components/toggle"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { toggleVariants } from "@repo/shadcn-ui/components/toggle";
import { cn } from "@repo/shadcn-ui/lib/utils";
import type { VariantProps } from "class-variance-authority";
import * as React from "react";
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & {
spacing?: number
orientation?: "horizontal" | "vertical"
spacing?: number;
orientation?: "horizontal" | "vertical";
}
>({
size: "default",
variant: "default",
spacing: 0,
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 (
<ToggleGroupPrimitive
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
data-orientation={orientation}
style={{ "--gap": spacing } as React.CSSProperties}
<ToggleGroupPrimitive.Root
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
)}
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}
>
<ToggleGroupContext.Provider
value={{ variant, size, spacing, orientation }}
>
<ToggleGroupContext.Provider value={{ variant, size, spacing, orientation }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive>
)
}
</ToggleGroupPrimitive.Root>
);
});
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
function ToggleGroupItem({
className,
children,
variant = "default",
size = "default",
...props
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<TogglePrimitive
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
data-spacing={context.spacing}
<ToggleGroupPrimitive.Item
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({
variant: context.variant || variant,
size: context.size || size,
variant: context.variant ?? variant ?? "default",
size: context.size ?? size ?? "default",
}),
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}
>
{children}
</TogglePrimitive>
)
}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem }
export { ToggleGroup, ToggleGroupItem };

View File

@ -1383,6 +1383,9 @@ importers:
'@hookform/resolvers':
specifier: ^5.2.2
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:
specifier: ^2.0.6
version: 2.0.6