docs: expandir documentação do README e adicionar importação em massa de lançamentos

- Expande README.md com estatísticas detalhadas do projeto (200 componentes, 15+ tabelas, 20+ widgets)
  - Adiciona descrição completa da stack técnica e versões
  - Documenta estrutura de diretórios de forma abrangente
  - Inclui diagramas de schema de banco de dados e fluxos de dados
  - Adiciona seção de destaques e funcionalidades recentes
  - Implementa diálogo de importação em massa de lançamentos (bulk-import-dialog.tsx)
  - Adiciona fontes AISans (Regular e Semibold) ao projeto
  - Remove classe bg-muted das páginas de autenticação
  - Adiciona /docs ao .gitignore
  - Limpa código não utilizado em componentes de lançamentos e páginas do dashboard
  - Atualiza dependências no package.json
This commit is contained in:
Felipe Coutinho
2026-01-05 13:01:18 +00:00
parent 4237062bde
commit 901e423959
24 changed files with 1342 additions and 199 deletions

View File

@@ -0,0 +1,363 @@
"use client";
import { createLancamentoAction } from "@/app/(dashboard)/lancamentos/actions";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { groupAndSortCategorias } from "@/lib/lancamentos/categoria-helpers";
import { useCallback, useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import type { LancamentoItem, SelectOption } from "../types";
import {
CategoriaSelectContent,
ContaCartaoSelectContent,
PagadorSelectContent,
} from "../select-items";
interface BulkImportDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
items: LancamentoItem[];
pagadorOptions: SelectOption[];
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
defaultPagadorId?: string | null;
}
export function BulkImportDialog({
open,
onOpenChange,
items,
pagadorOptions,
contaOptions,
cartaoOptions,
categoriaOptions,
defaultPagadorId,
}: BulkImportDialogProps) {
const [pagadorId, setPagadorId] = useState<string | undefined>(
defaultPagadorId ?? undefined
);
const [categoriaId, setCategoriaId] = useState<string | undefined>(undefined);
const [contaId, setContaId] = useState<string | undefined>(undefined);
const [cartaoId, setCartaoId] = useState<string | undefined>(undefined);
const [isPending, startTransition] = useTransition();
// Reset form when dialog opens/closes
const handleOpenChange = useCallback(
(newOpen: boolean) => {
if (!newOpen) {
setPagadorId(defaultPagadorId ?? undefined);
setCategoriaId(undefined);
setContaId(undefined);
setCartaoId(undefined);
}
onOpenChange(newOpen);
},
[onOpenChange, defaultPagadorId]
);
const categoriaGroups = useMemo(() => {
// Get unique transaction types from items
const transactionTypes = new Set(items.map((item) => item.transactionType));
// Filter categories based on transaction types
const filtered = categoriaOptions.filter((option) => {
if (!option.group) return false;
return Array.from(transactionTypes).some(
(type) => option.group?.toLowerCase() === type.toLowerCase()
);
});
return groupAndSortCategorias(filtered);
}, [categoriaOptions, items]);
const handleSubmit = useCallback(
async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!pagadorId) {
toast.error("Selecione o pagador.");
return;
}
if (!categoriaId) {
toast.error("Selecione a categoria.");
return;
}
startTransition(async () => {
let successCount = 0;
let errorCount = 0;
for (const item of items) {
const sanitizedAmount = Math.abs(item.amount);
// Determine payment method based on original item
const isCredit = item.paymentMethod === "Cartão de crédito";
// Validate payment method fields
if (isCredit && !cartaoId) {
toast.error("Selecione um cartão de crédito.");
return;
}
if (!isCredit && !contaId) {
toast.error("Selecione uma conta.");
return;
}
const payload = {
purchaseDate: item.purchaseDate,
period: item.period,
name: item.name,
transactionType: item.transactionType as "Despesa" | "Receita" | "Transferência",
amount: sanitizedAmount,
condition: item.condition as "À vista" | "Parcelado" | "Recorrente",
paymentMethod: item.paymentMethod as "Cartão de crédito" | "Cartão de débito" | "Pix" | "Dinheiro" | "Boleto" | "Pré-Pago | VR/VA" | "Transferência bancária",
pagadorId,
secondaryPagadorId: undefined,
isSplit: false,
contaId: isCredit ? undefined : contaId,
cartaoId: isCredit ? cartaoId : undefined,
categoriaId,
note: item.note || undefined,
isSettled: isCredit ? null : Boolean(item.isSettled),
installmentCount:
item.condition === "Parcelado" && item.installmentCount
? Number(item.installmentCount)
: undefined,
recurrenceCount:
item.condition === "Recorrente" && item.recurrenceCount
? Number(item.recurrenceCount)
: undefined,
dueDate:
item.paymentMethod === "Boleto" && item.dueDate
? item.dueDate
: undefined,
};
const result = await createLancamentoAction(payload as any);
if (result.success) {
successCount++;
} else {
errorCount++;
console.error(`Failed to import ${item.name}:`, result.error);
}
}
if (errorCount === 0) {
toast.success(
`${successCount} ${
successCount === 1 ? "lançamento importado" : "lançamentos importados"
} com sucesso!`
);
handleOpenChange(false);
} else if (successCount > 0) {
toast.warning(
`${successCount} importados, ${errorCount} falharam. Verifique o console para detalhes.`
);
} else {
toast.error("Falha ao importar lançamentos. Verifique o console.");
}
});
},
[items, pagadorId, categoriaId, contaId, cartaoId, handleOpenChange]
);
const itemCount = items.length;
const hasCredit = items.some((item) => item.paymentMethod === "Cartão de crédito");
const hasNonCredit = items.some((item) => item.paymentMethod !== "Cartão de crédito");
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Importar Lançamentos</DialogTitle>
<DialogDescription>
Importando {itemCount} {itemCount === 1 ? "lançamento" : "lançamentos"}.
Selecione o pagador, categoria e forma de pagamento para aplicar a todos.
</DialogDescription>
</DialogHeader>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label htmlFor="pagador">Pagador *</Label>
<Select value={pagadorId} onValueChange={setPagadorId}>
<SelectTrigger id="pagador" className="w-full">
<SelectValue placeholder="Selecione o pagador">
{pagadorId &&
(() => {
const selectedOption = pagadorOptions.find(
(opt) => opt.value === pagadorId
);
return selectedOption ? (
<PagadorSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{pagadorOptions.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 htmlFor="categoria">Categoria *</Label>
<Select value={categoriaId} onValueChange={setCategoriaId}>
<SelectTrigger id="categoria" className="w-full">
<SelectValue placeholder="Selecione a categoria">
{categoriaId &&
(() => {
const selectedOption = categoriaOptions.find(
(opt) => opt.value === categoriaId
);
return selectedOption ? (
<CategoriaSelectContent
label={selectedOption.label}
icon={selectedOption.icon}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{categoriaGroups.map((group) => (
<SelectGroup key={group.label}>
<SelectLabel>{group.label}</SelectLabel>
{group.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
<CategoriaSelectContent
label={option.label}
icon={option.icon}
/>
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
</div>
{hasNonCredit && (
<div className="space-y-2">
<Label htmlFor="conta">
Conta {hasCredit ? "(para não cartão)" : "*"}
</Label>
<Select value={contaId} onValueChange={setContaId}>
<SelectTrigger id="conta" className="w-full">
<SelectValue placeholder="Selecione a conta">
{contaId &&
(() => {
const selectedOption = contaOptions.find(
(opt) => opt.value === contaId
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{contaOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
/>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{hasCredit && (
<div className="space-y-2">
<Label htmlFor="cartao">
Cartão {hasNonCredit ? "(para cartão de crédito)" : "*"}
</Label>
<Select value={cartaoId} onValueChange={setCartaoId}>
<SelectTrigger id="cartao" className="w-full">
<SelectValue placeholder="Selecione o cartão">
{cartaoId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === cartaoId
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
/>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<DialogFooter className="gap-3">
<Button
type="button"
variant="outline"
onClick={() => handleOpenChange(false)}
disabled={isPending}
>
Cancelar
</Button>
<Button type="submit" disabled={isPending}>
{isPending ? "Importando..." : "Importar"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -22,6 +22,7 @@ export interface LancamentoDialogProps {
defaultPurchaseDate?: string | null;
lockCartaoSelection?: boolean;
lockPaymentMethod?: boolean;
isImporting?: boolean;
onBulkEditRequest?: (data: {
id: string;
name: string;

View File

@@ -62,6 +62,7 @@ export function LancamentoDialog({
defaultPurchaseDate,
lockCartaoSelection,
lockPaymentMethod,
isImporting = false,
onBulkEditRequest,
}: LancamentoDialogProps) {
const [dialogOpen, setDialogOpen] = useControlledState(
@@ -75,6 +76,7 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
isImporting,
})
);
const [periodDirty, setPeriodDirty] = useState(false);
@@ -92,6 +94,7 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
isImporting,
}
)
);
@@ -106,6 +109,7 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
isImporting,
]);
const primaryPagador = formState.pagadorId;
@@ -301,15 +305,20 @@ export function LancamentoDialog({
]
);
const isCopyMode = mode === "create" && Boolean(lancamento);
const isCopyMode = mode === "create" && Boolean(lancamento) && !isImporting;
const isImportMode = mode === "create" && Boolean(lancamento) && isImporting;
const title = mode === "create"
? isCopyMode
? isImportMode
? "Importar para Minha Conta"
: isCopyMode
? "Copiar lançamento"
: "Novo lançamento"
: "Editar lançamento";
const description =
mode === "create"
? isCopyMode
? isImportMode
? "Importando lançamento de outro usuário. Ajuste a categoria, pagador e cartão/conta antes de salvar."
: isCopyMode
? "Os dados do lançamento foram copiados. Revise e ajuste conforme necessário antes de salvar."
: "Informe os dados abaixo para registrar um novo lançamento."
: "Atualize as informações do lançamento selecionado.";

View File

@@ -15,6 +15,7 @@ import { toast } from "sonner";
import { AnticipateInstallmentsDialog } from "../dialogs/anticipate-installments-dialog/anticipate-installments-dialog";
import { AnticipationHistoryDialog } from "../dialogs/anticipate-installments-dialog/anticipation-history-dialog";
import { BulkActionDialog, type BulkActionScope } from "../dialogs/bulk-action-dialog";
import { BulkImportDialog } from "../dialogs/bulk-import-dialog";
import { LancamentoDetailsDialog } from "../dialogs/lancamento-details-dialog";
import { LancamentoDialog } from "../dialogs/lancamento-dialog/lancamento-dialog";
import { LancamentosTable } from "../table/lancamentos-table";
@@ -27,6 +28,7 @@ import type {
} from "../types";
interface LancamentosPageProps {
currentUserId: string;
lancamentos: LancamentoItem[];
pagadorOptions: SelectOption[];
splitPagadorOptions: SelectOption[];
@@ -44,9 +46,17 @@ interface LancamentosPageProps {
defaultPaymentMethod?: string | null;
lockCartaoSelection?: boolean;
lockPaymentMethod?: boolean;
// Opções específicas para o dialog de importação (quando visualizando dados de outro usuário)
importPagadorOptions?: SelectOption[];
importSplitPagadorOptions?: SelectOption[];
importDefaultPagadorId?: string | null;
importContaOptions?: SelectOption[];
importCartaoOptions?: SelectOption[];
importCategoriaOptions?: SelectOption[];
}
export function LancamentosPage({
currentUserId,
lancamentos,
pagadorOptions,
splitPagadorOptions,
@@ -64,6 +74,12 @@ export function LancamentosPage({
defaultPaymentMethod,
lockCartaoSelection,
lockPaymentMethod,
importPagadorOptions,
importSplitPagadorOptions,
importDefaultPagadorId,
importContaOptions,
importCartaoOptions,
importCategoriaOptions,
}: LancamentosPageProps) {
const [selectedLancamento, setSelectedLancamento] =
useState<LancamentoItem | null>(null);
@@ -72,6 +88,9 @@ export function LancamentosPage({
const [copyOpen, setCopyOpen] = useState(false);
const [lancamentoToCopy, setLancamentoToCopy] =
useState<LancamentoItem | null>(null);
const [importOpen, setImportOpen] = useState(false);
const [lancamentoToImport, setLancamentoToImport] =
useState<LancamentoItem | null>(null);
const [massAddOpen, setMassAddOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [lancamentoToDelete, setLancamentoToDelete] =
@@ -105,6 +124,8 @@ export function LancamentosPage({
const [anticipationHistoryOpen, setAnticipationHistoryOpen] = useState(false);
const [selectedForAnticipation, setSelectedForAnticipation] =
useState<LancamentoItem | null>(null);
const [bulkImportOpen, setBulkImportOpen] = useState(false);
const [lancamentosToImport, setLancamentosToImport] = useState<LancamentoItem[]>([]);
const handleToggleSettlement = useCallback(async (item: LancamentoItem) => {
if (item.paymentMethod === "Cartão de crédito") {
@@ -296,6 +317,16 @@ export function LancamentosPage({
setCopyOpen(true);
}, []);
const handleImport = useCallback((item: LancamentoItem) => {
setLancamentoToImport(item);
setImportOpen(true);
}, []);
const handleBulkImport = useCallback((items: LancamentoItem[]) => {
setLancamentosToImport(items);
setBulkImportOpen(true);
}, []);
const handleConfirmDelete = useCallback((item: LancamentoItem) => {
if (item.seriesId) {
setPendingDeleteData(item);
@@ -325,6 +356,7 @@ export function LancamentosPage({
<>
<LancamentosTable
data={lancamentos}
currentUserId={currentUserId}
pagadorFilterOptions={pagadorFilterOptions}
categoriaFilterOptions={categoriaFilterOptions}
contaCartaoFilterOptions={contaCartaoFilterOptions}
@@ -332,8 +364,10 @@ export function LancamentosPage({
onMassAdd={allowCreate ? handleMassAdd : undefined}
onEdit={handleEdit}
onCopy={handleCopy}
onImport={handleImport}
onConfirmDelete={handleConfirmDelete}
onBulkDelete={handleMultipleBulkDelete}
onBulkImport={handleBulkImport}
onViewDetails={handleViewDetails}
onToggleSettlement={handleToggleSettlement}
onAnticipate={handleAnticipate}
@@ -381,6 +415,38 @@ export function LancamentosPage({
defaultPeriod={selectedPeriod}
/>
<LancamentoDialog
mode="create"
open={importOpen && !!lancamentoToImport}
onOpenChange={(open) => {
setImportOpen(open);
if (!open) {
setLancamentoToImport(null);
}
}}
pagadorOptions={importPagadorOptions ?? pagadorOptions}
splitPagadorOptions={importSplitPagadorOptions ?? splitPagadorOptions}
defaultPagadorId={importDefaultPagadorId ?? defaultPagadorId}
contaOptions={importContaOptions ?? contaOptions}
cartaoOptions={importCartaoOptions ?? cartaoOptions}
categoriaOptions={importCategoriaOptions ?? categoriaOptions}
estabelecimentos={estabelecimentos}
lancamento={lancamentoToImport ?? undefined}
defaultPeriod={selectedPeriod}
isImporting={true}
/>
<BulkImportDialog
open={bulkImportOpen && lancamentosToImport.length > 0}
onOpenChange={setBulkImportOpen}
items={lancamentosToImport}
pagadorOptions={importPagadorOptions ?? pagadorOptions}
contaOptions={importContaOptions ?? contaOptions}
cartaoOptions={importCartaoOptions ?? cartaoOptions}
categoriaOptions={importCategoriaOptions ?? categoriaOptions}
defaultPagadorId={importDefaultPagadorId ?? defaultPagadorId}
/>
<LancamentoDialog
mode="update"
open={editOpen && !!selectedLancamento}

View File

@@ -91,8 +91,10 @@ const resolveLogoSrc = (logo: string | null) => {
};
type BuildColumnsArgs = {
currentUserId: string;
onEdit?: (item: LancamentoItem) => void;
onCopy?: (item: LancamentoItem) => void;
onImport?: (item: LancamentoItem) => void;
onConfirmDelete?: (item: LancamentoItem) => void;
onViewDetails?: (item: LancamentoItem) => void;
onToggleSettlement?: (item: LancamentoItem) => void;
@@ -103,8 +105,10 @@ type BuildColumnsArgs = {
};
const buildColumns = ({
currentUserId,
onEdit,
onCopy,
onImport,
onConfirmDelete,
onViewDetails,
onToggleSettlement,
@@ -116,6 +120,7 @@ const buildColumns = ({
const noop = () => undefined;
const handleEdit = onEdit ?? noop;
const handleCopy = onCopy ?? noop;
const handleImport = onImport ?? noop;
const handleConfirmDelete = onConfirmDelete ?? noop;
const handleViewDetails = onViewDetails ?? noop;
const handleToggleSettlement = onToggleSettlement ?? noop;
@@ -419,6 +424,7 @@ const buildColumns = ({
contaLogo,
cartaoId,
contaId,
userId,
} = row.original;
const label = cartaoName ?? contaName;
const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo);
@@ -428,20 +434,14 @@ const buildColumns = ({
? `/contas/${contaId}/extrato`
: null;
const Icon = cartaoId ? RiBankCard2Line : contaId ? RiBankLine : null;
const isOwnData = userId === currentUserId;
if (!label) {
return "—";
}
return (
<Link
href={href ?? "#"}
className={cn(
"flex items-center gap-2",
href ? "underline " : "pointer-events-none"
)}
aria-disabled={!href}
>
const content = (
<>
{logoSrc ? (
<Image
src={logoSrc}
@@ -455,6 +455,23 @@ const buildColumns = ({
{Icon ? (
<Icon className="size-4 text-muted-foreground" aria-hidden />
) : null}
</>
);
if (!isOwnData) {
return <div className="flex items-center gap-2">{content}</div>;
}
return (
<Link
href={href ?? "#"}
className={cn(
"flex items-center gap-2",
href ? "underline " : "pointer-events-none"
)}
aria-disabled={!href}
>
{content}
</Link>
);
},
@@ -526,30 +543,41 @@ const buildColumns = ({
<RiEyeLine className="size-4" />
Detalhes
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => handleEdit(row.original)}
disabled={row.original.readonly}
>
<RiPencilLine className="size-4" />
Editar
</DropdownMenuItem>
{row.original.categoriaName !== "Pagamentos" && (
{row.original.userId === currentUserId && (
<DropdownMenuItem
onSelect={() => handleEdit(row.original)}
disabled={row.original.readonly}
>
<RiPencilLine className="size-4" />
Editar
</DropdownMenuItem>
)}
{row.original.categoriaName !== "Pagamentos" && row.original.userId === currentUserId && (
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
<RiFileCopyLine className="size-4" />
Copiar
</DropdownMenuItem>
)}
<DropdownMenuItem
variant="destructive"
onSelect={() => handleConfirmDelete(row.original)}
disabled={row.original.readonly}
>
<RiDeleteBin5Line className="size-4" />
Remover
</DropdownMenuItem>
{row.original.categoriaName !== "Pagamentos" && row.original.userId !== currentUserId && (
<DropdownMenuItem onSelect={() => handleImport(row.original)}>
<RiFileCopyLine className="size-4" />
Importar para Minha Conta
</DropdownMenuItem>
)}
{row.original.userId === currentUserId && (
<DropdownMenuItem
variant="destructive"
onSelect={() => handleConfirmDelete(row.original)}
disabled={row.original.readonly}
>
<RiDeleteBin5Line className="size-4" />
Remover
</DropdownMenuItem>
)}
{/* Opções de Antecipação */}
{row.original.condition === "Parcelado" &&
{row.original.userId === currentUserId &&
row.original.condition === "Parcelado" &&
row.original.seriesId && (
<>
<DropdownMenuSeparator />
@@ -594,6 +622,7 @@ const buildColumns = ({
type LancamentosTableProps = {
data: LancamentoItem[];
currentUserId: string;
pagadorFilterOptions?: LancamentoFilterOption[];
categoriaFilterOptions?: LancamentoFilterOption[];
contaCartaoFilterOptions?: ContaCartaoFilterOption[];
@@ -601,8 +630,10 @@ type LancamentosTableProps = {
onMassAdd?: () => void;
onEdit?: (item: LancamentoItem) => void;
onCopy?: (item: LancamentoItem) => void;
onImport?: (item: LancamentoItem) => void;
onConfirmDelete?: (item: LancamentoItem) => void;
onBulkDelete?: (items: LancamentoItem[]) => void;
onBulkImport?: (items: LancamentoItem[]) => void;
onViewDetails?: (item: LancamentoItem) => void;
onToggleSettlement?: (item: LancamentoItem) => void;
onAnticipate?: (item: LancamentoItem) => void;
@@ -614,6 +645,7 @@ type LancamentosTableProps = {
export function LancamentosTable({
data,
currentUserId,
pagadorFilterOptions = [],
categoriaFilterOptions = [],
contaCartaoFilterOptions = [],
@@ -621,8 +653,10 @@ export function LancamentosTable({
onMassAdd,
onEdit,
onCopy,
onImport,
onConfirmDelete,
onBulkDelete,
onBulkImport,
onViewDetails,
onToggleSettlement,
onAnticipate,
@@ -643,8 +677,10 @@ export function LancamentosTable({
const columns = useMemo(
() =>
buildColumns({
currentUserId,
onEdit,
onCopy,
onImport,
onConfirmDelete,
onViewDetails,
onToggleSettlement,
@@ -654,8 +690,10 @@ export function LancamentosTable({
showActions,
}),
[
currentUserId,
onEdit,
onCopy,
onImport,
onConfirmDelete,
onViewDetails,
onToggleSettlement,
@@ -693,6 +731,10 @@ export function LancamentosTable({
0
);
// Check if all data belongs to current user to determine if filters should be shown
const isOwnData = data.every((item) => item.userId === currentUserId);
const shouldShowFilters = showFilters && isOwnData;
const handleBulkDelete = () => {
if (onBulkDelete && selectedCount > 0) {
const selectedItems = selectedRows.map((row) => row.original);
@@ -701,8 +743,16 @@ export function LancamentosTable({
}
};
const handleBulkImport = () => {
if (onBulkImport && selectedCount > 0) {
const selectedItems = selectedRows.map((row) => row.original);
onBulkImport(selectedItems);
setRowSelection({});
}
};
const showTopControls =
Boolean(onCreate) || Boolean(onMassAdd) || showFilters;
Boolean(onCreate) || Boolean(onMassAdd) || shouldShowFilters;
return (
<TooltipProvider>
@@ -738,10 +788,10 @@ export function LancamentosTable({
) : null}
</div>
) : (
<span className={showFilters ? "hidden sm:block" : ""} />
<span className={shouldShowFilters ? "hidden sm:block" : ""} />
)}
{showFilters ? (
{shouldShowFilters ? (
<LancamentosFilters
pagadorOptions={pagadorFilterOptions}
categoriaOptions={categoriaFilterOptions}
@@ -752,7 +802,7 @@ export function LancamentosTable({
</div>
) : null}
{selectedCount > 0 && onBulkDelete ? (
{selectedCount > 0 && onBulkDelete && selectedRows.every(row => row.original.userId === currentUserId) ? (
<div className="flex flex-wrap items-center gap-3 rounded-lg border bg-muted/50 px-4 py-2">
<div className="flex flex-col text-sm text-muted-foreground sm:flex-row sm:items-center sm:gap-2">
<span>
@@ -782,6 +832,36 @@ export function LancamentosTable({
</div>
) : null}
{selectedCount > 0 && onBulkImport && selectedRows.some(row => row.original.userId !== currentUserId) ? (
<div className="flex flex-wrap items-center gap-3 rounded-lg border bg-muted/50 px-4 py-2">
<div className="flex flex-col text-sm text-muted-foreground sm:flex-row sm:items-center sm:gap-2">
<span>
{selectedCount}{" "}
{selectedCount === 1 ? "item selecionado" : "itens selecionados"}
</span>
<span className="hidden sm:inline" aria-hidden>
-
</span>
<span>
Total:{" "}
<MoneyValues
amount={selectedTotal}
className="inline font-medium text-foreground"
/>
</span>
</div>
<Button
onClick={handleBulkImport}
variant="default"
size="sm"
className="ml-auto"
>
<RiFileCopyLine className="size-4" />
Importar selecionados
</Button>
</div>
) : null}
<Card className="py-2">
<CardContent className="px-2 py-4 sm:px-4">
{hasRows ? (

View File

@@ -1,5 +1,6 @@
export type LancamentoItem = {
id: string;
userId: string;
name: string;
purchaseDate: string;
period: string;

View File

@@ -147,7 +147,7 @@ export function PagadoresPage({
value={shareCodeInput}
onChange={(event) => setShareCodeInput(event.target.value)}
disabled={joinPending}
className="w-56"
className="w-56 border-dashed"
/>
<Button type="submit" disabled={joinPending}>
{joinPending ? "Adicionando..." : "Adicionar por código"}