feat(timesheet): add live search and searchable project selectors

This commit is contained in:
2026-04-29 01:25:05 +03:30
parent 8868b7d1cc
commit 05f2b4a4bb
5 changed files with 342 additions and 244 deletions

View File

@@ -18,7 +18,8 @@ export interface TimeEntryFilters {
interface TimesheetFilterBarProps {
searchQuery: string;
filters: TimeEntryFilters;
onApply: (searchQuery: string, filters: TimeEntryFilters) => void;
onSearchChange: (value: string) => void;
onApply: (filters: TimeEntryFilters) => void;
onClearFilters: () => void;
projects: Project[];
tags: Tag[];
@@ -41,6 +42,8 @@ interface TimesheetFilterBarProps {
tagPrefix?: string;
fromPrefix?: string;
toPrefix?: string;
searchTags?: string;
noTagsFound?: string;
};
}
@@ -49,11 +52,15 @@ function FilterTagMultiSelect({
selectedTagIds,
onChange,
title,
searchPlaceholder,
emptyLabel,
}: {
tags: Tag[];
selectedTagIds: string[];
onChange: (tagIds: string[]) => void;
title: string;
searchPlaceholder: string;
emptyLabel: string;
}) {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
@@ -145,7 +152,7 @@ function FilterTagMultiSelect({
type="text"
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder="Search tags..."
placeholder={searchPlaceholder}
className="h-8 w-full rounded-md border border-slate-200 bg-slate-50 pl-8 pr-2 text-xs text-slate-900 outline-none transition focus:border-sky-400 focus:bg-white focus:ring-2 focus:ring-sky-500/20 dark:border-slate-700 dark:bg-slate-800 dark:text-white dark:focus:border-sky-500 dark:focus:bg-slate-800"
/>
</div>
@@ -181,7 +188,7 @@ function FilterTagMultiSelect({
})}
{filteredTags.length === 0 && (
<div className="px-2 py-3 text-xs text-slate-500 dark:text-slate-400">
No tags found.
{emptyLabel}
</div>
)}
</div>
@@ -215,6 +222,7 @@ function MiniFilterBlock({
export default function TimesheetFilterBar({
searchQuery,
filters,
onSearchChange,
onApply,
onClearFilters,
projects,
@@ -223,13 +231,8 @@ export default function TimesheetFilterBar({
labels,
}: TimesheetFilterBarProps) {
const [isExpanded, setIsExpanded] = useState(false);
const [draftSearchQuery, setDraftSearchQuery] = useState(searchQuery);
const [draftFilters, setDraftFilters] = useState<TimeEntryFilters>(filters);
useEffect(() => {
setDraftSearchQuery(searchQuery);
}, [searchQuery]);
useEffect(() => {
setDraftFilters(filters);
}, [filters]);
@@ -275,8 +278,8 @@ export default function TimesheetFilterBar({
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 rtl:left-auto rtl:right-3" />
<input
type="text"
value={draftSearchQuery}
onChange={(event) => setDraftSearchQuery(event.target.value)}
value={searchQuery}
onChange={(event) => onSearchChange(event.target.value)}
placeholder={searchPlaceholder}
className="h-9 w-full rounded-md border border-slate-200 bg-slate-50 pl-9 pr-3 text-sm text-slate-900 outline-none transition focus:border-sky-400 focus:bg-white focus:ring-2 focus:ring-sky-500/20 dark:border-slate-700 dark:bg-slate-800 dark:text-white dark:focus:border-sky-500 dark:focus:bg-slate-800 rtl:pl-3 rtl:pr-9"
/>
@@ -304,7 +307,7 @@ export default function TimesheetFilterBar({
<button
type="button"
onClick={() => {
setDraftSearchQuery("");
onSearchChange("");
setDraftFilters({
projectId: "",
clientId: "",
@@ -389,6 +392,8 @@ export default function TimesheetFilterBar({
selectedTagIds={draftFilters.tagIds}
onChange={(tagIds) => setDraftFilters((current) => ({ ...current, tagIds }))}
title={labels?.allTags || "All tags"}
searchPlaceholder={labels?.searchTags || "Search tags..."}
emptyLabel={labels?.noTagsFound || "No tags found."}
/>
</MiniFilterBlock>
</div>
@@ -396,7 +401,7 @@ export default function TimesheetFilterBar({
<div className="mt-2 flex justify-end">
<button
type="button"
onClick={() => onApply(draftSearchQuery, draftFilters)}
onClick={() => onApply(draftFilters)}
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-md border bg-sky-50 border-sky-200 text-sky-700 dark:border-sky-500/30 dark:bg-sky-500/15 dark:text-sky-300 px-3 text-sm font-medium transition hover:border-sky-700 hover:bg-sky-700 hover:text-sky-100 dark:hover:border-sky-400 dark:hover:text-sky-900 dark:hover:bg-sky-400"
>
{labels?.apply || "Apply"}

View File

@@ -16,6 +16,7 @@ interface SearchableSelectProps {
options: SearchableSelectOption[];
placeholder?: string;
searchPlaceholder?: string;
emptyLabel?: string;
disabled?: boolean;
className?: string;
buttonClassName?: string;
@@ -27,6 +28,7 @@ export function SearchableSelect({
options,
placeholder = "",
searchPlaceholder = "Search...",
emptyLabel = "No results",
disabled = false,
className = "",
buttonClassName = "",
@@ -136,7 +138,7 @@ export function SearchableSelect({
</button>
))}
{filteredOptions.length === 0 && (
<div className="px-3 py-3 text-sm text-slate-500 dark:text-slate-400">No results</div>
<div className="px-3 py-3 text-sm text-slate-500 dark:text-slate-400">{emptyLabel}</div>
)}
</div>
</div>,