mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 19:01:47 +00:00
refactor(core): move app para src e padroniza estrutura
This commit is contained in:
@@ -0,0 +1,558 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RiCheckLine,
|
||||
RiExpandUpDownLine,
|
||||
RiFilter3Line,
|
||||
} from "@remixicon/react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import {
|
||||
LANCAMENTO_CONDITIONS,
|
||||
LANCAMENTO_PAYMENT_METHODS,
|
||||
LANCAMENTO_TRANSACTION_TYPES,
|
||||
} from "@/features/transactions/constants";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/shared/components/ui/command";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/shared/components/ui/drawer";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/shared/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
} from "@/shared/components/ui/select";
|
||||
import { cn } from "@/shared/utils/ui";
|
||||
import {
|
||||
CategoriaSelectContent,
|
||||
ConditionSelectContent,
|
||||
ContaCartaoSelectContent,
|
||||
PagadorSelectContent,
|
||||
PaymentMethodSelectContent,
|
||||
TransactionTypeSelectContent,
|
||||
} from "../select-items";
|
||||
import type { ContaCartaoFilterOption, LancamentoFilterOption } from "../types";
|
||||
|
||||
const FILTER_EMPTY_VALUE = "__all";
|
||||
|
||||
const buildStaticOptions = (values: readonly string[]) =>
|
||||
values.map((value) => ({ value, label: value }));
|
||||
|
||||
interface FilterSelectProps {
|
||||
param: string;
|
||||
placeholder: string;
|
||||
options: { value: string; label: string }[];
|
||||
widthClass?: string;
|
||||
disabled?: boolean;
|
||||
getParamValue: (key: string) => string;
|
||||
onChange: (key: string, value: string | null) => void;
|
||||
renderContent?: (label: string) => ReactNode;
|
||||
}
|
||||
|
||||
function FilterSelect({
|
||||
param,
|
||||
placeholder,
|
||||
options,
|
||||
widthClass = "w-[130px]",
|
||||
disabled,
|
||||
getParamValue,
|
||||
onChange,
|
||||
renderContent,
|
||||
}: FilterSelectProps) {
|
||||
const value = getParamValue(param);
|
||||
const current = options.find((option) => option.value === value);
|
||||
const displayLabel =
|
||||
value === FILTER_EMPTY_VALUE
|
||||
? placeholder
|
||||
: (current?.label ?? placeholder);
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(nextValue) =>
|
||||
onChange(param, nextValue === FILTER_EMPTY_VALUE ? null : nextValue)
|
||||
}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn("text-sm border-dashed", widthClass)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className="truncate">
|
||||
{value !== FILTER_EMPTY_VALUE && current && renderContent
|
||||
? renderContent(current.label)
|
||||
: displayLabel}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{renderContent ? renderContent(option.label) : option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
interface LancamentosFiltersProps {
|
||||
pagadorOptions: LancamentoFilterOption[];
|
||||
categoriaOptions: LancamentoFilterOption[];
|
||||
contaCartaoOptions: ContaCartaoFilterOption[];
|
||||
className?: string;
|
||||
exportButton?: ReactNode;
|
||||
hideAdvancedFilters?: boolean;
|
||||
}
|
||||
|
||||
export function LancamentosFilters({
|
||||
pagadorOptions,
|
||||
categoriaOptions,
|
||||
contaCartaoOptions,
|
||||
className,
|
||||
exportButton,
|
||||
hideAdvancedFilters = false,
|
||||
}: LancamentosFiltersProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const getParamValue = (key: string) =>
|
||||
searchParams.get(key) ?? FILTER_EMPTY_VALUE;
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(key: string, value: string | null) => {
|
||||
const nextParams = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (value && value !== FILTER_EMPTY_VALUE) {
|
||||
nextParams.set(key, value);
|
||||
} else {
|
||||
nextParams.delete(key);
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
router.replace(`${pathname}?${nextParams.toString()}`, {
|
||||
scroll: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
[searchParams, pathname, router],
|
||||
);
|
||||
|
||||
const [searchValue, setSearchValue] = useState(searchParams.get("q") ?? "");
|
||||
const currentSearchParam = searchParams.get("q") ?? "";
|
||||
|
||||
useEffect(() => {
|
||||
setSearchValue(currentSearchParam);
|
||||
}, [currentSearchParam]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchValue === currentSearchParam) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
const normalized = searchValue.trim();
|
||||
handleFilterChange("q", normalized.length > 0 ? normalized : null);
|
||||
}, 350);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [searchValue, currentSearchParam, handleFilterChange]);
|
||||
|
||||
const handleReset = () => {
|
||||
const periodValue = searchParams.get("periodo");
|
||||
const nextParams = new URLSearchParams();
|
||||
if (periodValue) {
|
||||
nextParams.set("periodo", periodValue);
|
||||
}
|
||||
setSearchValue("");
|
||||
setCategoriaOpen(false);
|
||||
startTransition(() => {
|
||||
const target = nextParams.toString()
|
||||
? `${pathname}?${nextParams.toString()}`
|
||||
: pathname;
|
||||
router.replace(target, { scroll: false });
|
||||
});
|
||||
};
|
||||
|
||||
const pagadorSelectOptions = pagadorOptions.map((option) => ({
|
||||
value: option.slug,
|
||||
label: option.label,
|
||||
avatarUrl: option.avatarUrl,
|
||||
}));
|
||||
|
||||
const contaOptions = contaCartaoOptions
|
||||
.filter((option) => option.kind === "conta")
|
||||
.map((option) => ({
|
||||
value: option.slug,
|
||||
label: option.label,
|
||||
logo: option.logo,
|
||||
}));
|
||||
|
||||
const cartaoOptions = contaCartaoOptions
|
||||
.filter((option) => option.kind === "cartao")
|
||||
.map((option) => ({
|
||||
value: option.slug,
|
||||
label: option.label,
|
||||
logo: option.logo,
|
||||
}));
|
||||
|
||||
const categoriaValue = getParamValue("categoria");
|
||||
const selectedCategoria =
|
||||
categoriaValue !== FILTER_EMPTY_VALUE
|
||||
? categoriaOptions.find((option) => option.slug === categoriaValue)
|
||||
: null;
|
||||
|
||||
const pagadorValue = getParamValue("pagador");
|
||||
const selectedPagador =
|
||||
pagadorValue !== FILTER_EMPTY_VALUE
|
||||
? pagadorOptions.find((option) => option.slug === pagadorValue)
|
||||
: null;
|
||||
|
||||
const contaCartaoValue = getParamValue("contaCartao");
|
||||
const selectedContaCartao =
|
||||
contaCartaoValue !== FILTER_EMPTY_VALUE
|
||||
? contaCartaoOptions.find((option) => option.slug === contaCartaoValue)
|
||||
: null;
|
||||
|
||||
const [categoriaOpen, setCategoriaOpen] = useState(false);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
const hasActiveFilters =
|
||||
searchParams.get("transacao") ||
|
||||
searchParams.get("condicao") ||
|
||||
searchParams.get("pagamento") ||
|
||||
searchParams.get("pagador") ||
|
||||
searchParams.get("categoria") ||
|
||||
searchParams.get("contaCartao");
|
||||
|
||||
const handleResetFilters = () => {
|
||||
handleReset();
|
||||
setDrawerOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-2 md:flex-row md:flex-wrap md:items-center",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
placeholder="Buscar"
|
||||
aria-label="Buscar lançamentos"
|
||||
className="w-full md:w-[250px] text-sm border-dashed"
|
||||
/>
|
||||
|
||||
<div className="flex w-full gap-2 md:w-auto">
|
||||
{exportButton && (
|
||||
<div className="flex-1 md:flex-none *:w-full *:md:w-auto">
|
||||
{exportButton}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hideAdvancedFilters && (
|
||||
<Drawer
|
||||
direction="right"
|
||||
open={drawerOpen}
|
||||
onOpenChange={setDrawerOpen}
|
||||
>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 md:flex-none text-sm border-dashed relative bg-transparent"
|
||||
aria-label="Abrir filtros"
|
||||
>
|
||||
<RiFilter3Line className="size-4" />
|
||||
Filtros
|
||||
{hasActiveFilters && (
|
||||
<span className="absolute -top-1 -right-1 size-3 rounded-full bg-primary" />
|
||||
)}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Filtros</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Selecione os filtros desejados para refinar os lançamentos
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Tipo de Lançamento
|
||||
</label>
|
||||
<FilterSelect
|
||||
param="transacao"
|
||||
placeholder="Todos"
|
||||
options={buildStaticOptions(LANCAMENTO_TRANSACTION_TYPES)}
|
||||
widthClass="w-full border-dashed"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => (
|
||||
<TransactionTypeSelectContent label={label} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Condição de Lançamento
|
||||
</label>
|
||||
<FilterSelect
|
||||
param="condicao"
|
||||
placeholder="Todas"
|
||||
options={buildStaticOptions(LANCAMENTO_CONDITIONS)}
|
||||
widthClass="w-full border-dashed"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => (
|
||||
<ConditionSelectContent label={label} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Forma de Pagamento
|
||||
</label>
|
||||
<FilterSelect
|
||||
param="pagamento"
|
||||
placeholder="Todos"
|
||||
options={buildStaticOptions(LANCAMENTO_PAYMENT_METHODS)}
|
||||
widthClass="w-full border-dashed"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => (
|
||||
<PaymentMethodSelectContent label={label} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Pagador</label>
|
||||
<Select
|
||||
value={getParamValue("pagador")}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange(
|
||||
"pagador",
|
||||
value === FILTER_EMPTY_VALUE ? null : value,
|
||||
)
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full text-sm border-dashed"
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedPagador ? (
|
||||
<PagadorSelectContent
|
||||
label={selectedPagador.label}
|
||||
avatarUrl={selectedPagador.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
"Todos"
|
||||
)}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
|
||||
{pagadorSelectOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<PagadorSelectContent
|
||||
label={option.label}
|
||||
avatarUrl={option.avatarUrl}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Categoria</label>
|
||||
<Popover
|
||||
open={categoriaOpen}
|
||||
onOpenChange={setCategoriaOpen}
|
||||
modal
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={categoriaOpen}
|
||||
className="w-full justify-between text-sm border-dashed"
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="truncate flex items-center gap-2">
|
||||
{selectedCategoria ? (
|
||||
<CategoriaSelectContent
|
||||
label={selectedCategoria.label}
|
||||
icon={selectedCategoria.icon}
|
||||
/>
|
||||
) : (
|
||||
"Todas"
|
||||
)}
|
||||
</span>
|
||||
<RiExpandUpDownLine className="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="w-[220px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Buscar categoria..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>Nada encontrado.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value={FILTER_EMPTY_VALUE}
|
||||
onSelect={() => {
|
||||
handleFilterChange("categoria", null);
|
||||
setCategoriaOpen(false);
|
||||
}}
|
||||
>
|
||||
Todas
|
||||
{categoriaValue === FILTER_EMPTY_VALUE ? (
|
||||
<RiCheckLine className="ml-auto size-4" />
|
||||
) : null}
|
||||
</CommandItem>
|
||||
{categoriaOptions.map((option) => (
|
||||
<CommandItem
|
||||
key={option.slug}
|
||||
value={option.slug}
|
||||
onSelect={() => {
|
||||
handleFilterChange("categoria", option.slug);
|
||||
setCategoriaOpen(false);
|
||||
}}
|
||||
>
|
||||
<CategoriaSelectContent
|
||||
label={option.label}
|
||||
icon={option.icon}
|
||||
/>
|
||||
{categoriaValue === option.slug ? (
|
||||
<RiCheckLine className="ml-auto size-4" />
|
||||
) : null}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Conta/Cartão</label>
|
||||
<Select
|
||||
value={getParamValue("contaCartao")}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange(
|
||||
"contaCartao",
|
||||
value === FILTER_EMPTY_VALUE ? null : value,
|
||||
)
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full text-sm border-dashed"
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedContaCartao ? (
|
||||
<ContaCartaoSelectContent
|
||||
label={selectedContaCartao.label}
|
||||
logo={selectedContaCartao.logo}
|
||||
isCartao={selectedContaCartao.kind === "cartao"}
|
||||
/>
|
||||
) : (
|
||||
"Todos"
|
||||
)}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
|
||||
{contaOptions.length > 0 ? (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Contas</SelectLabel>
|
||||
{contaOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<ContaCartaoSelectContent
|
||||
label={option.label}
|
||||
logo={option.logo}
|
||||
isCartao={false}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
) : null}
|
||||
{cartaoOptions.length > 0 ? (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Cartões</SelectLabel>
|
||||
{cartaoOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<ContaCartaoSelectContent
|
||||
label={option.label}
|
||||
logo={option.logo}
|
||||
isCartao={true}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
) : null}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleResetFilters}
|
||||
disabled={isPending || !hasActiveFilters}
|
||||
>
|
||||
Limpar filtros
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1090
src/features/transactions/components/table/transactions-table.tsx
Normal file
1090
src/features/transactions/components/table/transactions-table.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user