147 lines
4.9 KiB
Vue
147 lines
4.9 KiB
Vue
<script setup lang="ts">
|
|
import { ListFilter, Search } from '@lucide/vue';
|
|
import { computed } from 'vue';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from '@/components/ui/popover';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import type { DataTableFilterDef } from '@/types/data-table';
|
|
|
|
const props = defineProps<{
|
|
filterDefs?: DataTableFilterDef[];
|
|
filterValues?: Record<string, string>;
|
|
searchPlaceholder?: string;
|
|
}>();
|
|
|
|
const search = defineModel<string>('search', { default: '' });
|
|
|
|
const emit = defineEmits<{
|
|
'filter-change': [key: string, value: string];
|
|
'filters-reset': [];
|
|
}>();
|
|
|
|
const activeFilterCount = computed(() => {
|
|
if (!props.filterDefs?.length || !props.filterValues) {
|
|
return 0;
|
|
}
|
|
|
|
return props.filterDefs.filter((filter) => {
|
|
const value = props.filterValues?.[filter.key];
|
|
|
|
return value !== undefined && value !== '';
|
|
}).length;
|
|
});
|
|
|
|
function filterValue(key: string): string {
|
|
return props.filterValues?.[key] ?? '';
|
|
}
|
|
|
|
function onFilterChange(key: string, value: string): void {
|
|
emit('filter-change', key, value === 'all' ? '' : value);
|
|
}
|
|
|
|
function resetFilters(): void {
|
|
emit('filters-reset');
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div class="relative w-full max-w-sm">
|
|
<Search class="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
|
|
<Input
|
|
v-model="search"
|
|
type="search"
|
|
:placeholder="searchPlaceholder ?? 'Cari...'"
|
|
class="pl-9"
|
|
/>
|
|
</div>
|
|
|
|
<Popover v-if="filterDefs?.length">
|
|
<PopoverTrigger as-child>
|
|
<Button variant="outline" class="shrink-0">
|
|
<ListFilter class="size-4" />
|
|
Filter
|
|
<Badge
|
|
v-if="activeFilterCount > 0"
|
|
variant="secondary"
|
|
class="ml-1 size-5 rounded-full px-1 text-xs"
|
|
>
|
|
{{ activeFilterCount }}
|
|
</Badge>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent align="end" class="w-80 space-y-4 p-4">
|
|
<div class="space-y-1">
|
|
<h4 class="text-sm font-medium">
|
|
Filter
|
|
</h4>
|
|
<p class="text-muted-foreground text-xs">
|
|
Sesuaikan filter untuk mempersempit data.
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
v-for="filter in filterDefs"
|
|
:key="filter.key"
|
|
class="space-y-2"
|
|
>
|
|
<Label :for="`filter-${filter.key}`">{{ filter.label }}</Label>
|
|
|
|
<Input
|
|
v-if="filter.type === 'text'"
|
|
:id="`filter-${filter.key}`"
|
|
:model-value="filterValue(filter.key)"
|
|
:placeholder="filter.placeholder ?? `Filter ${filter.label.toLowerCase()}`"
|
|
@update:model-value="onFilterChange(filter.key, String($event ?? ''))"
|
|
/>
|
|
|
|
<Select
|
|
v-else
|
|
:model-value="filterValue(filter.key) || 'all'"
|
|
@update:model-value="onFilterChange(filter.key, String($event ?? ''))"
|
|
>
|
|
<SelectTrigger :id="`filter-${filter.key}`" class="w-full">
|
|
<SelectValue :placeholder="filter.placeholder ?? 'Semua'" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">
|
|
Semua
|
|
</SelectItem>
|
|
<SelectItem
|
|
v-for="option in filter.options"
|
|
:key="option.value"
|
|
:value="option.value"
|
|
>
|
|
{{ option.label }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<Button
|
|
v-if="activeFilterCount > 0"
|
|
variant="ghost"
|
|
size="sm"
|
|
class="w-full"
|
|
@click="resetFilters"
|
|
>
|
|
Reset Filter
|
|
</Button>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</template>
|