Simpl-Resultat/src/components/reports/CategoryZoomHeader.tsx
le king fu 01869462f4
All checks were successful
PR Check / rust (push) Successful in 21m2s
PR Check / frontend (push) Successful in 2m13s
PR Check / rust (pull_request) Successful in 21m14s
PR Check / frontend (pull_request) Successful in 2m9s
feat(reports/category): replace select with searchable combobox (#103)
Swap the native <select> in CategoryZoomHeader for the reusable
CategoryCombobox. Enhances the combobox with ARIA compliance
(combobox, listbox, option roles + aria-expanded, aria-controls,
aria-activedescendant) and hierarchy indentation based on parent_id
depth. Adds reports.category.searchPlaceholder in FR/EN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:07:47 -04:00

73 lines
2.4 KiB
TypeScript

import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getAllCategoriesWithCounts } from "../../services/categoryService";
import CategoryCombobox from "../shared/CategoryCombobox";
import type { Category } from "../../shared/types";
export interface CategoryZoomHeaderProps {
categoryId: number | null;
includeSubcategories: boolean;
onCategoryChange: (id: number | null) => void;
onIncludeSubcategoriesChange: (flag: boolean) => void;
}
export default function CategoryZoomHeader({
categoryId,
includeSubcategories,
onCategoryChange,
onIncludeSubcategoriesChange,
}: CategoryZoomHeaderProps) {
const { t } = useTranslation();
const [categories, setCategories] = useState<Category[]>([]);
useEffect(() => {
getAllCategoriesWithCounts()
.then((rows) =>
setCategories(
rows.map((r) => ({
id: r.id,
name: r.name,
parent_id: r.parent_id ?? undefined,
color: r.color ?? undefined,
icon: r.icon ?? undefined,
type: r.type,
is_active: r.is_active,
is_inputable: r.is_inputable,
sort_order: r.sort_order,
created_at: "",
})),
),
)
.catch(() => setCategories([]));
}, []);
return (
<div className="flex flex-col sm:flex-row sm:items-center gap-3 bg-[var(--card)] border border-[var(--border)] rounded-xl p-4">
<label className="flex flex-col gap-1 flex-1 min-w-0">
<span className="text-xs font-medium text-[var(--muted-foreground)] uppercase tracking-wide">
{t("reports.category.selectCategory")}
</span>
<CategoryCombobox
categories={categories}
value={categoryId}
onChange={onCategoryChange}
placeholder={t("reports.category.searchPlaceholder")}
ariaLabel={t("reports.category.selectCategory")}
/>
</label>
<label className="inline-flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={includeSubcategories}
onChange={(e) => onIncludeSubcategoriesChange(e.target.checked)}
className="accent-[var(--primary)]"
/>
<span>
{includeSubcategories
? t("reports.category.includeSubcategories")
: t("reports.category.directOnly")}
</span>
</label>
</div>
);
}