satupeta-main/app/(modules)/admin/_components/search-action-bar.tsx
2026-01-27 09:31:12 +07:00

227 lines
8.2 KiB
TypeScript

import { Button } from "@/shared/components/ds/button";
import Link from "next/link";
import { CirclePlusIcon, ListFilter, UnlinkIcon, X } from "lucide-react";
import { SearchInput } from "./search-input";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/components/ui/popover";
import { useRouter, useSearchParams } from "next/navigation";
import { useState, useEffect } from "react";
import { Badge } from "@/shared/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select";
import { useAuthSession } from "@/shared/hooks/use-session";
interface FilterOption {
label: string;
value: string;
group: string;
groupLabel: string;
}
interface SearchAndActionBarProps {
searchValue: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
buttonLabel: string;
buttonLink: string;
placeholder?: string;
selectedCount?: number;
onBulkAction?: () => void;
bulkLabel?: string;
showBulkAction?: boolean;
filterOptions?: FilterOption[];
}
const SearchAndActionBar = ({
searchValue,
onChange,
buttonLabel,
buttonLink,
placeholder = "Masukkan kata kunci",
selectedCount = 0,
onBulkAction,
bulkLabel = "Nonaktifkan",
showBulkAction = false,
filterOptions = [],
}: SearchAndActionBarProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [isFilterOpen, setIsFilterOpen] = useState(false);
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
const { session } = useAuthSession();
const userRole = session?.user?.role;
// Initialize filters from URL params
useEffect(() => {
const filterParam = searchParams.get("filter");
if (filterParam) {
try {
const filters = JSON.parse(filterParam);
// Convert nested array format to flat array for internal state
const flatFilters = Array.isArray(filters) ? filters.flatMap((f) => (Array.isArray(f) ? f : [f])) : [];
setSelectedFilters(flatFilters);
} catch {
setSelectedFilters([]);
}
}
}, [searchParams]);
const handleFilterChange = (option: FilterOption) => {
const filterValue = `${option.group}=${option.value}`;
// If the filter is already selected, remove it
if (selectedFilters.includes(filterValue)) {
const newFilters = selectedFilters.filter((f) => f !== filterValue);
setSelectedFilters(newFilters);
updateUrlFilters(newFilters);
} else {
// If the filter is not selected, add it
const newFilters = [...selectedFilters, filterValue];
setSelectedFilters(newFilters);
updateUrlFilters(newFilters);
}
};
const removeFilter = (value: string) => {
const newFilters = selectedFilters.filter((f) => f !== value);
setSelectedFilters(newFilters);
updateUrlFilters(newFilters);
};
const updateUrlFilters = (filters: string[]) => {
const newParams = new URLSearchParams(searchParams.toString());
if (filters.length > 0) {
// Group filters by their group property
const groupedFilters = filters.reduce((acc, filter) => {
const [group] = filter.split("=");
if (!acc[group]) {
acc[group] = [];
}
acc[group].push(filter);
return acc;
}, {} as Record<string, string[]>);
// Convert grouped filters to array format
const filterString = JSON.stringify(Object.values(groupedFilters));
newParams.set("filter", filterString);
} else {
newParams.delete("filter");
}
router.push(`?${newParams.toString()}`);
};
// Group filter options by their group property
const groupedOptions = filterOptions.reduce((acc, option) => {
const group = option.group;
if (!acc[group]) {
acc[group] = {
label: option.groupLabel,
options: [],
};
}
acc[group].options.push(option);
return acc;
}, {} as Record<string, { label: string; options: FilterOption[] }>);
return (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex space-x-2">
<SearchInput placeholder={placeholder} value={searchValue} onChange={onChange} className="w-full max-w-sm" />
{filterOptions.length > 0 && (
<Popover open={isFilterOpen} onOpenChange={setIsFilterOpen}>
<PopoverTrigger asChild>
<Button className="bg-white text-zinc-950 text-sm gap-3 border border-zinc-200 px-4 py-2">
<ListFilter className="w-4 h-4" />
Filter
{filterOptions.length > 0 && selectedFilters.length > 0 && (
<Badge className="ml-2">
{
selectedFilters.filter((filter) => {
const [key, value] = filter.split("=");
return filterOptions.some((opt) => opt.group === key && opt.value === value);
}).length
}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-4">
<h4 className="font-medium">Filter</h4>
<div className="flex flex-col space-y-2 max-h-[300px] overflow-y-auto">
{Object.entries(groupedOptions).map(([group, { label, options }]) => (
<div key={group} className="space-y-2">
<h5 className="text-sm font-medium text-muted-foreground">{label}</h5>
<Select
onValueChange={(value) => {
const option = options.find((opt) => opt.value === value);
if (option) handleFilterChange(option);
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder={`Select ${label}`} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
))}
</div>
</div>
</PopoverContent>
</Popover>
)}
</div>
<div className="flex items-center gap-2">
{showBulkAction && (
<Button disabled={selectedCount === 0} onClick={onBulkAction} className="bg-white text-zinc-950 border border-zinc-200 border-dashed rounded-lg">
<UnlinkIcon className="h-4 w-4 mr-2 text-zinc-950" />
<span className="text-zinc-950">{bulkLabel}</span>
</Button>
)}
{userRole.name !== "data_viewer" && (
<Link href={buttonLink}>
<Button size="sm">
<CirclePlusIcon className="h-4 w-4 mr-2" />
{buttonLabel}
</Button>
</Link>
)}
</div>
</div>
{/* Active Filters */}
{selectedFilters.length > 0 && (
<div className="flex flex-wrap gap-2">
{selectedFilters
.filter((filter) => {
const [key, value] = filter.split("=");
return filterOptions.some((opt) => opt.group === key && opt.value === value);
})
.map((filter) => {
const [key, value] = filter.split("=");
const option = filterOptions.find((opt) => opt.group === key && opt.value === value);
return (
<Badge key={filter} className="flex items-center gap-1 px-3 py-1">
{option?.label || filter}
<button onClick={() => removeFilter(filter)} className="ml-1 rounded-full outline-none focus:ring-2 focus:ring-ring">
<X className="h-3 w-3" />
</button>
</Badge>
);
})}
</div>
)}
</div>
);
};
export default SearchAndActionBar;