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:
@@ -188,3 +188,89 @@ export async function deleteBudgetAction(
|
|||||||
return handleActionError(error);
|
return handleActionError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const duplicatePreviousMonthSchema = z.object({
|
||||||
|
period: periodSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
type DuplicatePreviousMonthInput = z.infer<
|
||||||
|
typeof duplicatePreviousMonthSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export async function duplicatePreviousMonthBudgetsAction(
|
||||||
|
input: DuplicatePreviousMonthInput
|
||||||
|
): Promise<ActionResult> {
|
||||||
|
try {
|
||||||
|
const user = await getUser();
|
||||||
|
const data = duplicatePreviousMonthSchema.parse(input);
|
||||||
|
|
||||||
|
// Calcular mês anterior
|
||||||
|
const [year, month] = data.period.split("-").map(Number);
|
||||||
|
const currentDate = new Date(year, month - 1, 1);
|
||||||
|
const previousDate = new Date(currentDate);
|
||||||
|
previousDate.setMonth(previousDate.getMonth() - 1);
|
||||||
|
|
||||||
|
const prevYear = previousDate.getFullYear();
|
||||||
|
const prevMonth = String(previousDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const previousPeriod = `${prevYear}-${prevMonth}`;
|
||||||
|
|
||||||
|
// Buscar orçamentos do mês anterior
|
||||||
|
const previousBudgets = await db.query.orcamentos.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(orcamentos.userId, user.id),
|
||||||
|
eq(orcamentos.period, previousPeriod)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (previousBudgets.length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Não foram encontrados orçamentos no mês anterior.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar orçamentos existentes do mês atual
|
||||||
|
const currentBudgets = await db.query.orcamentos.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(orcamentos.userId, user.id),
|
||||||
|
eq(orcamentos.period, data.period)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtrar para evitar duplicatas
|
||||||
|
const existingCategoryIds = new Set(
|
||||||
|
currentBudgets.map((b) => b.categoriaId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const budgetsToCopy = previousBudgets.filter(
|
||||||
|
(b) => b.categoriaId && !existingCategoryIds.has(b.categoriaId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (budgetsToCopy.length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
"Todas as categorias do mês anterior já possuem orçamento neste mês.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserir novos orçamentos
|
||||||
|
await db.insert(orcamentos).values(
|
||||||
|
budgetsToCopy.map((b) => ({
|
||||||
|
amount: b.amount,
|
||||||
|
period: data.period,
|
||||||
|
userId: user.id,
|
||||||
|
categoriaId: b.categoriaId!,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
revalidateForEntity("orcamentos");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `${budgetsToCopy.length} orçamento${budgetsToCopy.length > 1 ? "s" : ""} duplicado${budgetsToCopy.length > 1 ? "s" : ""} com sucesso.`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleActionError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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}
|
pagadorFilterOptions={pagadorFilterOptions}
|
||||||
categoriaFilterOptions={categoriaFilterOptions}
|
categoriaFilterOptions={categoriaFilterOptions}
|
||||||
contaCartaoFilterOptions={contaCartaoFilterOptions}
|
contaCartaoFilterOptions={contaCartaoFilterOptions}
|
||||||
|
selectedPeriod={selectedPeriod}
|
||||||
onCreate={allowCreate ? handleCreate : undefined}
|
onCreate={allowCreate ? handleCreate : undefined}
|
||||||
onMassAdd={allowCreate ? handleMassAdd : undefined}
|
onMassAdd={allowCreate ? handleMassAdd : undefined}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerDescription,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerTrigger,
|
||||||
|
} from "@/components/ui/drawer";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -47,7 +56,11 @@ import {
|
|||||||
TransactionTypeSelectContent,
|
TransactionTypeSelectContent,
|
||||||
} from "../select-items";
|
} from "../select-items";
|
||||||
|
|
||||||
import { RiCheckLine, RiExpandUpDownLine } from "@remixicon/react";
|
import {
|
||||||
|
RiCheckLine,
|
||||||
|
RiExpandUpDownLine,
|
||||||
|
RiFilter3Line,
|
||||||
|
} from "@remixicon/react";
|
||||||
import type { ContaCartaoFilterOption, LancamentoFilterOption } from "../types";
|
import type { ContaCartaoFilterOption, LancamentoFilterOption } from "../types";
|
||||||
|
|
||||||
const FILTER_EMPTY_VALUE = "__all";
|
const FILTER_EMPTY_VALUE = "__all";
|
||||||
@@ -116,6 +129,7 @@ interface LancamentosFiltersProps {
|
|||||||
categoriaOptions: LancamentoFilterOption[];
|
categoriaOptions: LancamentoFilterOption[];
|
||||||
contaCartaoOptions: ContaCartaoFilterOption[];
|
contaCartaoOptions: ContaCartaoFilterOption[];
|
||||||
className?: string;
|
className?: string;
|
||||||
|
exportButton?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LancamentosFilters({
|
export function LancamentosFilters({
|
||||||
@@ -123,6 +137,7 @@ export function LancamentosFilters({
|
|||||||
categoriaOptions,
|
categoriaOptions,
|
||||||
contaCartaoOptions,
|
contaCartaoOptions,
|
||||||
className,
|
className,
|
||||||
|
exportButton,
|
||||||
}: LancamentosFiltersProps) {
|
}: LancamentosFiltersProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -242,222 +257,290 @@ export function LancamentosFilters({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const [categoriaOpen, setCategoriaOpen] = useState(false);
|
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 (
|
return (
|
||||||
<div className={cn("flex flex-wrap items-center gap-2", className)}>
|
<div className={cn("flex flex-wrap items-center gap-2", className)}>
|
||||||
<FilterSelect
|
{exportButton}
|
||||||
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} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FilterSelect
|
<Drawer direction="right" open={drawerOpen} onOpenChange={setDrawerOpen}>
|
||||||
param="condicao"
|
<DrawerTrigger asChild>
|
||||||
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>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
className="text-sm border-dashed relative"
|
||||||
aria-expanded={categoriaOpen}
|
aria-label="Abrir filtros"
|
||||||
className="w-[150px] justify-between text-sm border-dashed border-input"
|
|
||||||
disabled={isPending}
|
|
||||||
>
|
>
|
||||||
<span className="truncate flex items-center gap-2">
|
<RiFilter3Line className="size-4" />
|
||||||
{selectedCategoria ? (
|
Filtros
|
||||||
<CategoriaSelectContent
|
{hasActiveFilters && (
|
||||||
label={selectedCategoria.label}
|
<span className="absolute -top-1 -right-1 size-2 rounded-full bg-primary" />
|
||||||
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"
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</Button>
|
||||||
</SelectTrigger>
|
</DrawerTrigger>
|
||||||
<SelectContent>
|
<DrawerContent>
|
||||||
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
|
<DrawerHeader>
|
||||||
{contaOptions.length > 0 ? (
|
<DrawerTitle>Filtros</DrawerTitle>
|
||||||
<SelectGroup>
|
<DrawerDescription>
|
||||||
<SelectLabel>Contas</SelectLabel>
|
Selecione os filtros desejados para refinar os lançamentos
|
||||||
{contaOptions.map((option) => (
|
</DrawerDescription>
|
||||||
<SelectItem key={option.value} value={option.value}>
|
</DrawerHeader>
|
||||||
<ContaCartaoSelectContent
|
|
||||||
label={option.label}
|
<div className="flex-1 overflow-y-auto px-4 space-y-4">
|
||||||
logo={option.logo}
|
<div className="space-y-2">
|
||||||
isCartao={false}
|
<label className="text-sm font-medium">Tipo de Lançamento</label>
|
||||||
/>
|
<FilterSelect
|
||||||
</SelectItem>
|
param="transacao"
|
||||||
))}
|
placeholder="Todos"
|
||||||
</SelectGroup>
|
options={buildStaticOptions(LANCAMENTO_TRANSACTION_TYPES)}
|
||||||
) : null}
|
widthClass="w-full border-dashed"
|
||||||
{cartaoOptions.length > 0 ? (
|
disabled={isPending}
|
||||||
<SelectGroup>
|
getParamValue={getParamValue}
|
||||||
<SelectLabel>Cartões</SelectLabel>
|
onChange={handleFilterChange}
|
||||||
{cartaoOptions.map((option) => (
|
renderContent={(label) => (
|
||||||
<SelectItem key={option.value} value={option.value}>
|
<TransactionTypeSelectContent label={label} />
|
||||||
<ContaCartaoSelectContent
|
)}
|
||||||
label={option.label}
|
/>
|
||||||
logo={option.logo}
|
</div>
|
||||||
isCartao={true}
|
|
||||||
/>
|
<div className="space-y-2">
|
||||||
</SelectItem>
|
<label className="text-sm font-medium">Condição</label>
|
||||||
))}
|
<FilterSelect
|
||||||
</SelectGroup>
|
param="condicao"
|
||||||
) : null}
|
placeholder="Todas"
|
||||||
</SelectContent>
|
options={buildStaticOptions(LANCAMENTO_CONDITIONS)}
|
||||||
</Select>
|
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
|
<Input
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(event) => setSearchValue(event.target.value)}
|
onChange={(event) => setSearchValue(event.target.value)}
|
||||||
placeholder="Buscar"
|
placeholder="Buscar"
|
||||||
aria-label="Buscar lançamentos"
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ import type {
|
|||||||
LancamentoFilterOption,
|
LancamentoFilterOption,
|
||||||
LancamentoItem,
|
LancamentoItem,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import { LancamentosExport } from "../lancamentos-export";
|
||||||
import { LancamentosFilters } from "./lancamentos-filters";
|
import { LancamentosFilters } from "./lancamentos-filters";
|
||||||
|
|
||||||
const resolveLogoSrc = (logo: string | null) => {
|
const resolveLogoSrc = (logo: string | null) => {
|
||||||
@@ -626,6 +627,7 @@ type LancamentosTableProps = {
|
|||||||
pagadorFilterOptions?: LancamentoFilterOption[];
|
pagadorFilterOptions?: LancamentoFilterOption[];
|
||||||
categoriaFilterOptions?: LancamentoFilterOption[];
|
categoriaFilterOptions?: LancamentoFilterOption[];
|
||||||
contaCartaoFilterOptions?: ContaCartaoFilterOption[];
|
contaCartaoFilterOptions?: ContaCartaoFilterOption[];
|
||||||
|
selectedPeriod?: string;
|
||||||
onCreate?: () => void;
|
onCreate?: () => void;
|
||||||
onMassAdd?: () => void;
|
onMassAdd?: () => void;
|
||||||
onEdit?: (item: LancamentoItem) => void;
|
onEdit?: (item: LancamentoItem) => void;
|
||||||
@@ -649,6 +651,7 @@ export function LancamentosTable({
|
|||||||
pagadorFilterOptions = [],
|
pagadorFilterOptions = [],
|
||||||
categoriaFilterOptions = [],
|
categoriaFilterOptions = [],
|
||||||
contaCartaoFilterOptions = [],
|
contaCartaoFilterOptions = [],
|
||||||
|
selectedPeriod,
|
||||||
onCreate,
|
onCreate,
|
||||||
onMassAdd,
|
onMassAdd,
|
||||||
onEdit,
|
onEdit,
|
||||||
@@ -797,6 +800,14 @@ export function LancamentosTable({
|
|||||||
categoriaOptions={categoriaFilterOptions}
|
categoriaOptions={categoriaFilterOptions}
|
||||||
contaCartaoOptions={contaCartaoFilterOptions}
|
contaCartaoOptions={contaCartaoFilterOptions}
|
||||||
className="w-full lg:flex-1 lg:justify-end"
|
className="w-full lg:flex-1 lg:justify-end"
|
||||||
|
exportButton={
|
||||||
|
selectedPeriod ? (
|
||||||
|
<LancamentosExport
|
||||||
|
lancamentos={data}
|
||||||
|
period={selectedPeriod}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteBudgetAction } from "@/app/(dashboard)/orcamentos/actions";
|
import {
|
||||||
|
deleteBudgetAction,
|
||||||
|
duplicatePreviousMonthBudgetsAction,
|
||||||
|
} from "@/app/(dashboard)/orcamentos/actions";
|
||||||
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RiAddCircleLine, RiFundsLine } from "@remixicon/react";
|
import { RiAddCircleLine, RiFileCopyLine, RiFundsLine } from "@remixicon/react";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
@@ -29,6 +32,7 @@ export function BudgetsPage({
|
|||||||
const [selectedBudget, setSelectedBudget] = useState<Budget | null>(null);
|
const [selectedBudget, setSelectedBudget] = useState<Budget | null>(null);
|
||||||
const [removeOpen, setRemoveOpen] = useState(false);
|
const [removeOpen, setRemoveOpen] = useState(false);
|
||||||
const [budgetToRemove, setBudgetToRemove] = useState<Budget | null>(null);
|
const [budgetToRemove, setBudgetToRemove] = useState<Budget | null>(null);
|
||||||
|
const [duplicateOpen, setDuplicateOpen] = useState(false);
|
||||||
|
|
||||||
const hasBudgets = budgets.length > 0;
|
const hasBudgets = budgets.length > 0;
|
||||||
|
|
||||||
@@ -72,6 +76,21 @@ export function BudgetsPage({
|
|||||||
throw new Error(result.error);
|
throw new Error(result.error);
|
||||||
}, [budgetToRemove]);
|
}, [budgetToRemove]);
|
||||||
|
|
||||||
|
const handleDuplicateConfirm = useCallback(async () => {
|
||||||
|
const result = await duplicatePreviousMonthBudgetsAction({
|
||||||
|
period: selectedPeriod,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(result.message);
|
||||||
|
setDuplicateOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(result.error);
|
||||||
|
throw new Error(result.error);
|
||||||
|
}, [selectedPeriod]);
|
||||||
|
|
||||||
const removeTitle = budgetToRemove
|
const removeTitle = budgetToRemove
|
||||||
? `Remover orçamento de "${
|
? `Remover orçamento de "${
|
||||||
budgetToRemove.category?.name ?? "categoria removida"
|
budgetToRemove.category?.name ?? "categoria removida"
|
||||||
@@ -86,7 +105,7 @@ export function BudgetsPage({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start gap-4">
|
||||||
<BudgetDialog
|
<BudgetDialog
|
||||||
mode="create"
|
mode="create"
|
||||||
categories={categories}
|
categories={categories}
|
||||||
@@ -98,6 +117,14 @@ export function BudgetsPage({
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={categories.length === 0}
|
||||||
|
onClick={() => setDuplicateOpen(true)}
|
||||||
|
>
|
||||||
|
<RiFileCopyLine className="size-4" />
|
||||||
|
Copiar orçamentos do último mês
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasBudgets ? (
|
{hasBudgets ? (
|
||||||
@@ -142,6 +169,16 @@ export function BudgetsPage({
|
|||||||
confirmVariant="destructive"
|
confirmVariant="destructive"
|
||||||
onConfirm={handleRemoveConfirm}
|
onConfirm={handleRemoveConfirm}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmActionDialog
|
||||||
|
open={duplicateOpen}
|
||||||
|
onOpenChange={setDuplicateOpen}
|
||||||
|
title="Copiar orçamentos do último mês?"
|
||||||
|
description="Isso copiará os limites definidos no mês anterior para as categorias que ainda não possuem orçamento neste mês."
|
||||||
|
confirmLabel="Copiar orçamentos"
|
||||||
|
pendingLabel="Copiando..."
|
||||||
|
onConfirm={handleDuplicateConfirm}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user