feat: implementar melhorias em filtros, orçamentos e exportação
Refatoração de Filtros: - Move filtros select para Drawer lateral direito - Mantém busca fora do Drawer para acesso rápido - Adiciona indicador visual de filtros ativos - Implementa aplicação instantânea de filtros - Adiciona border-dashed e aumenta input de busca Cópia de Orçamentos: - Implementa funcionalidade de copiar orçamentos do mês anterior - Adiciona server action com validações e tratamento de erros - Cria modal de confirmação para a ação - Evita duplicações automáticas Exportação de Lançamentos: - Adiciona exportação em CSV, XLSX e PDF - Integra botão de exportação nos filtros - Segue padrão de Relatórios de Categorias - Inclui formatação específica por formato
This commit is contained in:
308
components/lancamentos/lancamentos-export.tsx
Normal file
308
components/lancamentos/lancamentos-export.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { formatCurrency } from "@/lib/lancamentos/formatting-helpers";
|
||||
import {
|
||||
RiDownloadLine,
|
||||
RiFileExcelLine,
|
||||
RiFilePdfLine,
|
||||
RiFileTextLine,
|
||||
} from "@remixicon/react";
|
||||
import { toast } from "sonner";
|
||||
import { useState } from "react";
|
||||
import jsPDF from "jspdf";
|
||||
import autoTable from "jspdf-autotable";
|
||||
import * as XLSX from "xlsx";
|
||||
import type { LancamentoItem } from "./types";
|
||||
|
||||
interface LancamentosExportProps {
|
||||
lancamentos: LancamentoItem[];
|
||||
period: string;
|
||||
}
|
||||
|
||||
export function LancamentosExport({
|
||||
lancamentos,
|
||||
period,
|
||||
}: LancamentosExportProps) {
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const getFileName = (extension: string) => {
|
||||
return `lancamentos-${period}.${extension}`;
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const getContaCartaoName = (lancamento: LancamentoItem) => {
|
||||
if (lancamento.contaName) return lancamento.contaName;
|
||||
if (lancamento.cartaoName) return lancamento.cartaoName;
|
||||
return "-";
|
||||
};
|
||||
|
||||
const exportToCSV = () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
const headers = [
|
||||
"Data",
|
||||
"Nome",
|
||||
"Tipo",
|
||||
"Condição",
|
||||
"Pagamento",
|
||||
"Valor",
|
||||
"Categoria",
|
||||
"Conta/Cartão",
|
||||
"Pagador",
|
||||
];
|
||||
const rows: string[][] = [];
|
||||
|
||||
lancamentos.forEach((lancamento) => {
|
||||
const row = [
|
||||
formatDate(lancamento.purchaseDate),
|
||||
lancamento.name,
|
||||
lancamento.transactionType,
|
||||
lancamento.condition,
|
||||
lancamento.paymentMethod,
|
||||
formatCurrency(lancamento.amount),
|
||||
lancamento.categoriaName ?? "-",
|
||||
getContaCartaoName(lancamento),
|
||||
lancamento.pagadorName ?? "-",
|
||||
];
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
const csvContent = [
|
||||
headers.join(","),
|
||||
...rows.map((row) => row.map((cell) => `"${cell}"`).join(",")),
|
||||
].join("\n");
|
||||
|
||||
const blob = new Blob(["\uFEFF" + csvContent], {
|
||||
type: "text/csv;charset=utf-8;",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = getFileName("csv");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.success("Lançamentos exportados em CSV com sucesso!");
|
||||
} catch (error) {
|
||||
console.error("Error exporting to CSV:", error);
|
||||
toast.error("Erro ao exportar lançamentos em CSV");
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const exportToExcel = () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
const headers = [
|
||||
"Data",
|
||||
"Nome",
|
||||
"Tipo",
|
||||
"Condição",
|
||||
"Pagamento",
|
||||
"Valor",
|
||||
"Categoria",
|
||||
"Conta/Cartão",
|
||||
"Pagador",
|
||||
];
|
||||
const rows: (string | number)[][] = [];
|
||||
|
||||
lancamentos.forEach((lancamento) => {
|
||||
const row = [
|
||||
formatDate(lancamento.purchaseDate),
|
||||
lancamento.name,
|
||||
lancamento.transactionType,
|
||||
lancamento.condition,
|
||||
lancamento.paymentMethod,
|
||||
lancamento.amount,
|
||||
lancamento.categoriaName ?? "-",
|
||||
getContaCartaoName(lancamento),
|
||||
lancamento.pagadorName ?? "-",
|
||||
];
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
const ws = XLSX.utils.aoa_to_sheet([headers, ...rows]);
|
||||
|
||||
ws["!cols"] = [
|
||||
{ wch: 12 }, // Data
|
||||
{ wch: 30 }, // Nome
|
||||
{ wch: 15 }, // Tipo
|
||||
{ wch: 15 }, // Condição
|
||||
{ wch: 20 }, // Pagamento
|
||||
{ wch: 15 }, // Valor
|
||||
{ wch: 20 }, // Categoria
|
||||
{ wch: 20 }, // Conta/Cartão
|
||||
{ wch: 20 }, // Pagador
|
||||
];
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Lançamentos");
|
||||
XLSX.writeFile(wb, getFileName("xlsx"));
|
||||
|
||||
toast.success("Lançamentos exportados em Excel com sucesso!");
|
||||
} catch (error) {
|
||||
console.error("Error exporting to Excel:", error);
|
||||
toast.error("Erro ao exportar lançamentos em Excel");
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const exportToPDF = () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
const doc = new jsPDF({ orientation: "landscape" });
|
||||
|
||||
doc.setFontSize(16);
|
||||
doc.text("Lançamentos", 14, 15);
|
||||
|
||||
doc.setFontSize(10);
|
||||
const periodParts = period.split("-");
|
||||
const monthNames = [
|
||||
"Janeiro",
|
||||
"Fevereiro",
|
||||
"Março",
|
||||
"Abril",
|
||||
"Maio",
|
||||
"Junho",
|
||||
"Julho",
|
||||
"Agosto",
|
||||
"Setembro",
|
||||
"Outubro",
|
||||
"Novembro",
|
||||
"Dezembro",
|
||||
];
|
||||
const formattedPeriod =
|
||||
periodParts.length === 2
|
||||
? `${monthNames[Number.parseInt(periodParts[1]) - 1]}/${periodParts[0]}`
|
||||
: period;
|
||||
doc.text(`Período: ${formattedPeriod}`, 14, 22);
|
||||
doc.text(`Gerado em: ${new Date().toLocaleDateString("pt-BR")}`, 14, 27);
|
||||
|
||||
const headers = [
|
||||
[
|
||||
"Data",
|
||||
"Nome",
|
||||
"Tipo",
|
||||
"Condição",
|
||||
"Pagamento",
|
||||
"Valor",
|
||||
"Categoria",
|
||||
"Conta/Cartão",
|
||||
"Pagador",
|
||||
],
|
||||
];
|
||||
|
||||
const body = lancamentos.map((lancamento) => [
|
||||
formatDate(lancamento.purchaseDate),
|
||||
lancamento.name,
|
||||
lancamento.transactionType,
|
||||
lancamento.condition,
|
||||
lancamento.paymentMethod,
|
||||
formatCurrency(lancamento.amount),
|
||||
lancamento.categoriaName ?? "-",
|
||||
getContaCartaoName(lancamento),
|
||||
lancamento.pagadorName ?? "-",
|
||||
]);
|
||||
|
||||
autoTable(doc, {
|
||||
head: headers,
|
||||
body: body,
|
||||
startY: 32,
|
||||
styles: {
|
||||
fontSize: 8,
|
||||
cellPadding: 2,
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [59, 130, 246],
|
||||
textColor: 255,
|
||||
fontStyle: "bold",
|
||||
},
|
||||
columnStyles: {
|
||||
0: { cellWidth: 20 }, // Data
|
||||
1: { cellWidth: 40 }, // Nome
|
||||
2: { cellWidth: 25 }, // Tipo
|
||||
3: { cellWidth: 25 }, // Condição
|
||||
4: { cellWidth: 30 }, // Pagamento
|
||||
5: { cellWidth: 25 }, // Valor
|
||||
6: { cellWidth: 30 }, // Categoria
|
||||
7: { cellWidth: 30 }, // Conta/Cartão
|
||||
8: { cellWidth: 30 }, // Pagador
|
||||
},
|
||||
didParseCell: (cellData) => {
|
||||
if (cellData.section === "body" && cellData.column.index === 5) {
|
||||
const lancamento = lancamentos[cellData.row.index];
|
||||
if (lancamento) {
|
||||
if (lancamento.transactionType === "Despesa") {
|
||||
cellData.cell.styles.textColor = [220, 38, 38];
|
||||
} else if (lancamento.transactionType === "Receita") {
|
||||
cellData.cell.styles.textColor = [22, 163, 74];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
margin: { top: 32 },
|
||||
});
|
||||
|
||||
doc.save(getFileName("pdf"));
|
||||
|
||||
toast.success("Lançamentos exportados em PDF com sucesso!");
|
||||
} catch (error) {
|
||||
console.error("Error exporting to PDF:", error);
|
||||
toast.error("Erro ao exportar lançamentos em PDF");
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-sm border-dashed"
|
||||
disabled={isExporting || lancamentos.length === 0}
|
||||
aria-label="Exportar lançamentos"
|
||||
>
|
||||
<RiDownloadLine className="size-4" />
|
||||
{isExporting ? "Exportando..." : "Exportar"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={exportToCSV} disabled={isExporting}>
|
||||
<RiFileTextLine className="mr-2 h-4 w-4" />
|
||||
Exportar como CSV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={exportToExcel} disabled={isExporting}>
|
||||
<RiFileExcelLine className="mr-2 h-4 w-4" />
|
||||
Exportar como Excel (.xlsx)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={exportToPDF} disabled={isExporting}>
|
||||
<RiFilePdfLine className="mr-2 h-4 w-4" />
|
||||
Exportar como PDF
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -360,6 +360,7 @@ export function LancamentosPage({
|
||||
pagadorFilterOptions={pagadorFilterOptions}
|
||||
categoriaFilterOptions={categoriaFilterOptions}
|
||||
contaCartaoFilterOptions={contaCartaoFilterOptions}
|
||||
selectedPeriod={selectedPeriod}
|
||||
onCreate={allowCreate ? handleCreate : undefined}
|
||||
onMassAdd={allowCreate ? handleMassAdd : undefined}
|
||||
onEdit={handleEdit}
|
||||
|
||||
@@ -9,6 +9,15 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
@@ -47,7 +56,11 @@ import {
|
||||
TransactionTypeSelectContent,
|
||||
} from "../select-items";
|
||||
|
||||
import { RiCheckLine, RiExpandUpDownLine } from "@remixicon/react";
|
||||
import {
|
||||
RiCheckLine,
|
||||
RiExpandUpDownLine,
|
||||
RiFilter3Line,
|
||||
} from "@remixicon/react";
|
||||
import type { ContaCartaoFilterOption, LancamentoFilterOption } from "../types";
|
||||
|
||||
const FILTER_EMPTY_VALUE = "__all";
|
||||
@@ -116,6 +129,7 @@ interface LancamentosFiltersProps {
|
||||
categoriaOptions: LancamentoFilterOption[];
|
||||
contaCartaoOptions: ContaCartaoFilterOption[];
|
||||
className?: string;
|
||||
exportButton?: ReactNode;
|
||||
}
|
||||
|
||||
export function LancamentosFilters({
|
||||
@@ -123,6 +137,7 @@ export function LancamentosFilters({
|
||||
categoriaOptions,
|
||||
contaCartaoOptions,
|
||||
className,
|
||||
exportButton,
|
||||
}: LancamentosFiltersProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -242,222 +257,290 @@ export function LancamentosFilters({
|
||||
: null;
|
||||
|
||||
const [categoriaOpen, setCategoriaOpen] = useState(false);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
const hasActiveFilters = useMemo(() => {
|
||||
return (
|
||||
searchParams.get("transacao") ||
|
||||
searchParams.get("condicao") ||
|
||||
searchParams.get("pagamento") ||
|
||||
searchParams.get("pagador") ||
|
||||
searchParams.get("categoria") ||
|
||||
searchParams.get("contaCartao")
|
||||
);
|
||||
}, [searchParams]);
|
||||
|
||||
const handleResetFilters = useCallback(() => {
|
||||
handleReset();
|
||||
setDrawerOpen(false);
|
||||
}, [handleReset]);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-wrap items-center gap-2", className)}>
|
||||
<FilterSelect
|
||||
param="transacao"
|
||||
placeholder="Tipo de Lançamento"
|
||||
options={buildStaticOptions(LANCAMENTO_TRANSACTION_TYPES)}
|
||||
widthClass="w-[130px]"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => (
|
||||
<TransactionTypeSelectContent label={label} />
|
||||
)}
|
||||
/>
|
||||
{exportButton}
|
||||
|
||||
<FilterSelect
|
||||
param="condicao"
|
||||
placeholder="Condição"
|
||||
options={buildStaticOptions(LANCAMENTO_CONDITIONS)}
|
||||
widthClass="w-[130px]"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => <ConditionSelectContent label={label} />}
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
param="pagamento"
|
||||
placeholder="Pagamento"
|
||||
options={buildStaticOptions(LANCAMENTO_PAYMENT_METHODS)}
|
||||
widthClass="w-[130px]"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => <PaymentMethodSelectContent label={label} />}
|
||||
/>
|
||||
|
||||
<Select
|
||||
value={getParamValue("pagador")}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange(
|
||||
"pagador",
|
||||
value === FILTER_EMPTY_VALUE ? null : value
|
||||
)
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-[150px] text-sm border-dashed"
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedPagador ? (
|
||||
<PagadorSelectContent
|
||||
label={selectedPagador.label}
|
||||
avatarUrl={selectedPagador.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
"Pagador"
|
||||
)}
|
||||
</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>
|
||||
|
||||
<Popover open={categoriaOpen} onOpenChange={setCategoriaOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Drawer direction="right" open={drawerOpen} onOpenChange={setDrawerOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={categoriaOpen}
|
||||
className="w-[150px] justify-between text-sm border-dashed border-input"
|
||||
disabled={isPending}
|
||||
className="text-sm border-dashed relative"
|
||||
aria-label="Abrir filtros"
|
||||
>
|
||||
<span className="truncate flex items-center gap-2">
|
||||
{selectedCategoria ? (
|
||||
<CategoriaSelectContent
|
||||
label={selectedCategoria.label}
|
||||
icon={selectedCategoria.icon}
|
||||
/>
|
||||
) : (
|
||||
"Categoria"
|
||||
)}
|
||||
</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>
|
||||
|
||||
<Select
|
||||
value={getParamValue("contaCartao")}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange(
|
||||
"contaCartao",
|
||||
value === FILTER_EMPTY_VALUE ? null : value
|
||||
)
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-[150px] text-sm border-dashed"
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedContaCartao ? (
|
||||
<ContaCartaoSelectContent
|
||||
label={selectedContaCartao.label}
|
||||
logo={selectedContaCartao.logo}
|
||||
isCartao={selectedContaCartao.kind === "cartao"}
|
||||
/>
|
||||
) : (
|
||||
"Conta/Cartão"
|
||||
<RiFilter3Line className="size-4" />
|
||||
Filtros
|
||||
{hasActiveFilters && (
|
||||
<span className="absolute -top-1 -right-1 size-2 rounded-full bg-primary" />
|
||||
)}
|
||||
</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>
|
||||
</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</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">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}>
|
||||
<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>
|
||||
|
||||
<Input
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
placeholder="Buscar"
|
||||
aria-label="Buscar lançamentos"
|
||||
className="w-[150px] text-sm border-dashed"
|
||||
className="w-[250px] text-sm border-dashed"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
disabled={isPending}
|
||||
>
|
||||
Limpar
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ import type {
|
||||
LancamentoFilterOption,
|
||||
LancamentoItem,
|
||||
} from "../types";
|
||||
import { LancamentosExport } from "../lancamentos-export";
|
||||
import { LancamentosFilters } from "./lancamentos-filters";
|
||||
|
||||
const resolveLogoSrc = (logo: string | null) => {
|
||||
@@ -626,6 +627,7 @@ type LancamentosTableProps = {
|
||||
pagadorFilterOptions?: LancamentoFilterOption[];
|
||||
categoriaFilterOptions?: LancamentoFilterOption[];
|
||||
contaCartaoFilterOptions?: ContaCartaoFilterOption[];
|
||||
selectedPeriod?: string;
|
||||
onCreate?: () => void;
|
||||
onMassAdd?: () => void;
|
||||
onEdit?: (item: LancamentoItem) => void;
|
||||
@@ -649,6 +651,7 @@ export function LancamentosTable({
|
||||
pagadorFilterOptions = [],
|
||||
categoriaFilterOptions = [],
|
||||
contaCartaoFilterOptions = [],
|
||||
selectedPeriod,
|
||||
onCreate,
|
||||
onMassAdd,
|
||||
onEdit,
|
||||
@@ -797,6 +800,14 @@ export function LancamentosTable({
|
||||
categoriaOptions={categoriaFilterOptions}
|
||||
contaCartaoOptions={contaCartaoFilterOptions}
|
||||
className="w-full lg:flex-1 lg:justify-end"
|
||||
exportButton={
|
||||
selectedPeriod ? (
|
||||
<LancamentosExport
|
||||
lancamentos={data}
|
||||
period={selectedPeriod}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user