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:
363
components/lancamentos/dialogs/bulk-import-dialog.tsx
Normal file
363
components/lancamentos/dialogs/bulk-import-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export interface LancamentoDialogProps {
|
||||
defaultPurchaseDate?: string | null;
|
||||
lockCartaoSelection?: boolean;
|
||||
lockPaymentMethod?: boolean;
|
||||
isImporting?: boolean;
|
||||
onBulkEditRequest?: (data: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export type LancamentoItem = {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
purchaseDate: string;
|
||||
period: string;
|
||||
|
||||
Reference in New Issue
Block a user