feat: implementar melhorias em importação, compartilhamento e contas inativas

- Corrigir cálculo de valor na importação de lançamentos parcelados
    - Exibir valor total (parcela × quantidade) ao invés do valor da parcela individual
    - Permite recriar parcelamentos importados com valor correto

  - Permitir que usuários compartilhados se descompartilhem de pagadores
    - Adicionar componente PagadorLeaveShareCard na aba Perfil
    - Usuário filho pode sair do compartilhamento sem precisar do usuário pai
    - Manter autorização bidirecionada na action de remoção de share

  - Implementar submenu "Inativos" para contas bancárias
    - Criar página /contas/inativos seguindo padrão de cartões
    - Filtrar contas ativas e inativas em páginas separadas
    - Adicionar ícone e navegação no sidebar
This commit is contained in:
Felipe Coutinho
2026-01-11 22:44:20 +00:00
parent 147857c5bd
commit 6a45a5110d
26 changed files with 812 additions and 405 deletions

View File

@@ -1,4 +1,4 @@
import { lancamentos, pagadorShares, user as usersTable } from "@/db/schema";
import { lancamentos, pagadorShares, user as usersTable, contas, cartoes, categorias, pagadores } from "@/db/schema";
import { db } from "@/lib/db";
import { and, desc, eq, type SQL } from "drizzle-orm";
@@ -37,17 +37,54 @@ export async function fetchPagadorShares(
}));
}
export async function fetchPagadorLancamentos(filters: SQL[]) {
const lancamentoRows = await db.query.lancamentos.findMany({
where: and(...filters),
with: {
pagador: true,
conta: true,
cartao: true,
categoria: true,
export async function fetchCurrentUserShare(
pagadorId: string,
userId: string
): Promise<{ id: string; createdAt: string } | null> {
const shareRow = await db.query.pagadorShares.findFirst({
columns: {
id: true,
createdAt: true,
},
orderBy: desc(lancamentos.purchaseDate),
where: and(
eq(pagadorShares.pagadorId, pagadorId),
eq(pagadorShares.sharedWithUserId, userId)
),
});
return lancamentoRows;
if (!shareRow) {
return null;
}
return {
id: shareRow.id,
createdAt: shareRow.createdAt?.toISOString() ?? new Date().toISOString(),
};
}
export async function fetchPagadorLancamentos(filters: SQL[]) {
const lancamentoRows = await db
.select({
lancamento: lancamentos,
pagador: pagadores,
conta: contas,
cartao: cartoes,
categoria: categorias,
})
.from(lancamentos)
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id))
.leftJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
.where(and(...filters))
.orderBy(desc(lancamentos.purchaseDate), desc(lancamentos.createdAt));
// Transformar resultado para o formato esperado
return lancamentoRows.map((row: any) => ({
...row.lancamento,
pagador: row.pagador,
conta: row.conta,
cartao: row.cartao,
categoria: row.categoria,
}));
}

View File

@@ -5,6 +5,7 @@ import { PagadorInfoCard } from "@/components/pagadores/details/pagador-info-car
import { PagadorMonthlySummaryCard } from "@/components/pagadores/details/pagador-monthly-summary-card";
import { PagadorBoletoCard } from "@/components/pagadores/details/pagador-payment-method-cards";
import { PagadorSharingCard } from "@/components/pagadores/details/pagador-sharing-card";
import { PagadorLeaveShareCard } from "@/components/pagadores/details/pagador-leave-share-card";
import { LancamentosPage as LancamentosSection } from "@/components/lancamentos/page/lancamentos-page";
import type {
ContaCartaoFilterOption,
@@ -39,7 +40,7 @@ import {
fetchPagadorMonthlyBreakdown,
} from "@/lib/pagadores/details";
import { notFound } from "next/navigation";
import { fetchPagadorLancamentos, fetchPagadorShares } from "./data";
import { fetchPagadorLancamentos, fetchPagadorShares, fetchCurrentUserShare } from "./data";
type PageSearchParams = Promise<ResolvedSearchParams>;
@@ -92,9 +93,13 @@ export default async function Page({ params, searchParams }: PageProps) {
} = parsePeriodParam(periodoParamRaw);
const periodLabel = `${capitalize(monthName)} de ${year}`;
const allSearchFilters = extractLancamentoSearchFilters(resolvedSearchParams);
const searchFilters = canEdit
? extractLancamentoSearchFilters(resolvedSearchParams)
: EMPTY_FILTERS;
? allSearchFilters
: {
...EMPTY_FILTERS,
searchFilter: allSearchFilters.searchFilter, // Permitir busca mesmo em modo read-only
};
let filterSources: Awaited<
ReturnType<typeof fetchLancamentoFilterSources>
@@ -133,6 +138,10 @@ export default async function Page({ params, searchParams }: PageProps) {
? fetchPagadorShares(pagador.id)
: Promise.resolve([]);
const currentUserSharePromise = !canEdit
? fetchCurrentUserShare(pagador.id, userId)
: Promise.resolve(null);
const [
lancamentoRows,
monthlyBreakdown,
@@ -140,6 +149,7 @@ export default async function Page({ params, searchParams }: PageProps) {
cardUsage,
boletoStats,
shareRows,
currentUserShare,
estabelecimentos,
] = await Promise.all([
fetchPagadorLancamentos(filters),
@@ -164,6 +174,7 @@ export default async function Page({ params, searchParams }: PageProps) {
period: selectedPeriod,
}),
sharesPromise,
currentUserSharePromise,
getRecentEstablishmentsAction(),
]);
@@ -281,6 +292,13 @@ export default async function Page({ params, searchParams }: PageProps) {
shares={pagadorSharesData}
/>
) : null}
{!canEdit && currentUserShare ? (
<PagadorLeaveShareCard
shareId={currentUserShare.id}
pagadorName={pagadorData.name}
createdAt={currentUserShare.createdAt}
/>
) : null}
</TabsContent>
<TabsContent value="painel" className="space-y-4">