diff --git a/apps/dashboard/src/components/modals/edit-category-modal.tsx b/apps/dashboard/src/components/modals/edit-category-modal.tsx index bb1194a52..e6ada4095 100644 --- a/apps/dashboard/src/components/modals/edit-category-modal.tsx +++ b/apps/dashboard/src/components/modals/edit-category-modal.tsx @@ -3,7 +3,6 @@ import { updateCategorySchema, } from "@/actions/schema"; import { updateCategoryAction } from "@/actions/update-category-action"; -import { VatAssistant } from "@/components/vat-assistant"; import { zodResolver } from "@hookform/resolvers/zod"; import { Button } from "@midday/ui/button"; import { diff --git a/apps/dashboard/src/components/select-category.tsx b/apps/dashboard/src/components/select-category.tsx index f97cb7049..f2b978a69 100644 --- a/apps/dashboard/src/components/select-category.tsx +++ b/apps/dashboard/src/components/select-category.tsx @@ -1,9 +1,11 @@ import { createCategoriesAction } from "@/actions/create-categories-action"; -import { searchAction } from "@/actions/search-action"; import { getColorFromName } from "@/utils/categories"; -import { cn } from "@midday/ui/cn"; -import { Combobox } from "@midday/ui/combobox"; -import { useDebounce } from "@uidotdev/usehooks"; +import { createClient } from "@midday/supabase/client"; +import { + getCategoriesQuery, + getCurrentUserTeamQuery, +} from "@midday/supabase/queries"; +import { ComboboxDropdown } from "@midday/ui/combobox-dropdown"; import { useAction } from "next-safe-action/hooks"; import { useEffect, useState } from "react"; import { CategoryColor } from "./category"; @@ -17,131 +19,102 @@ type Selected = { type Props = { selected?: Selected; - placeholder: string; onChange: (selected: Selected) => void; }; -export function SelectCategory({ selected, placeholder, onChange }: Props) { - const [query, setQuery] = useState(""); +function transformCategory(category) { + return { + id: category.id, + label: category.name, + color: category.color, + slug: category.slug, + }; +} + +export function SelectCategory({ selected, onChange }: Props) { const [data, setData] = useState([]); - const [isLoading, setLoading] = useState(false); + const supabase = createClient(); + + useEffect(() => { + async function fetchData() { + const { data: userData } = await getCurrentUserTeamQuery(supabase); + if (userData?.team_id) { + const response = await getCategoriesQuery(supabase, { + teamId: userData.team_id, + limit: 1000, + }); + + if (response.data) { + setData(response.data.map(transformCategory)); + } + } + } + + if (!data.length) { + fetchData(); + } + }, [data]); const createCategories = useAction(createCategoriesAction, { onSuccess: (data) => { const category = data.at(0); if (category) { - setData((prev) => [category, ...prev]); + setData((prev) => [transformCategory(category), ...prev]); onChange(category); } }, }); - const debouncedSearchTerm = useDebounce(query, 50); + const selectedValue = selected ? transformCategory(selected) : undefined; - const search = useAction(searchAction, { - onSuccess: (response) => { - setData(response); - setLoading(false); - }, - onError: () => setLoading(false), - }); - - useEffect(() => { - if (debouncedSearchTerm) { - search.execute({ - query: debouncedSearchTerm, - type: "categories", - limit: 10, - }); - } - }, [debouncedSearchTerm]); - - const options = data?.map((option) => ({ - id: option.id, - name: option.name, - data: option, - component: () => { - return ( + return ( + { + onChange({ + id: item.id, + name: item.label, + color: item.color, + slug: item.slug, + }); + }} + onCreate={(value) => { + createCategories.execute({ + categories: [ + { + name: value, + color: getColorFromName(value), + }, + ], + }); + }} + renderSelectedItem={(selectedItem) => (
-
- {option.name} + + {selectedItem.label}
- ); - }, - })); - - const onSelect = (option) => { - onChange({ - id: option.id, - name: option.name, - color: option.data.color, - slug: option.data.slug, - }); - }; - - const onCreate = (value: string) => { - createCategories.execute({ - categories: [ - { - name: value, - color: getColorFromName(value), - }, - ], - }); - }; - - const selectedValue = selected - ? { - id: selected.id, - name: selected.name, - } - : undefined; - - return ( -
- {selected && ( - )} - - ( + renderOnCreate={(value) => { + return (
-
+ {`Create "${value}"`}
- )} - onCreate={onCreate} - onValueChange={(q) => { - if (q) { - setLoading(true); - setQuery(q); - } else { - setLoading(false); - } - }} - onSelect={onSelect} - options={options} - isLoading={isLoading} - classNameList="mt-2" - /> -
+ ); + }} + renderListItem={({ item }) => { + return ( +
+ + {item.label} +
+ ); + }} + /> ); } diff --git a/apps/dashboard/src/components/sheets/transaction-sheet.tsx b/apps/dashboard/src/components/sheets/transaction-sheet.tsx index a28d45eac..d5247eea6 100644 --- a/apps/dashboard/src/components/sheets/transaction-sheet.tsx +++ b/apps/dashboard/src/components/sheets/transaction-sheet.tsx @@ -1,4 +1,4 @@ -import { UpdateTransactionValues } from "@/actions/schema"; +import type { UpdateTransactionValues } from "@/actions/schema"; import { useMediaQuery } from "@/hooks/use-media-query"; import { Drawer, DrawerContent } from "@midday/ui/drawer"; import { Sheet, SheetContent } from "@midday/ui/sheet"; diff --git a/apps/dashboard/src/components/transaction-details.tsx b/apps/dashboard/src/components/transaction-details.tsx index 29b9fb87e..0377cf943 100644 --- a/apps/dashboard/src/components/transaction-details.tsx +++ b/apps/dashboard/src/components/transaction-details.tsx @@ -221,8 +221,6 @@ export function TransactionDetails({ = { + placeholder?: React.ReactNode; + searchPlaceholder?: string; + items: T[]; + onSelect: (item: T) => void; + selectedItem?: T; + renderSelectedItem?: (selectedItem: T) => React.ReactNode; + renderOnCreate?: (value: string) => React.ReactNode; + renderListItem?: (listItem: { + isChecked: boolean; + item: T; + }) => React.ReactNode; + emptyResults?: React.ReactNode; + popoverProps?: React.ComponentProps; + disabled?: boolean; + onCreate?: (value: string) => void; +}; + +export function ComboboxDropdown({ + placeholder, + searchPlaceholder, + items, + onSelect, + selectedItem: incomingSelectedItem, + renderSelectedItem, + renderListItem, + renderOnCreate, + emptyResults, + popoverProps, + disabled, + onCreate, +}: Props) { + const [open, setOpen] = React.useState(false); + const [internalSelectedItem, setInternalSelectedItem] = React.useState< + T | undefined + >(); + const [inputValue, setInputValue] = React.useState(""); + + const selectedItem = incomingSelectedItem ?? internalSelectedItem; + + const filteredItems = items.filter((item) => + item.label.toLowerCase().includes(inputValue.toLowerCase()) + ); + + const showCreate = + onCreate && + inputValue && + !items?.find((o) => o.label.toLowerCase() === inputValue.toLowerCase()); + + return ( + + + + + + + + + + {emptyResults ?? "No item found"} + + + {filteredItems.map((item) => { + const isChecked = selectedItem?.id === item.id; + + return ( + { + const foundItem = items.find((item) => item.id === id); + + if (!foundItem) { + return; + } + + onSelect(foundItem); + setInternalSelectedItem(foundItem); + setOpen(false); + }} + > + {renderListItem ? ( + renderListItem({ isChecked, item }) + ) : ( + <> + + {item.label} + + )} + + ); + })} + + {showCreate && ( + { + onCreate(inputValue); + setOpen(false); + setInputValue(""); + }} + onMouseDown={(event) => { + event.preventDefault(); + event.stopPropagation(); + }} + > + {renderOnCreate ? renderOnCreate(inputValue) : null} + + )} + + + + + + ); +}