refactor(ui): unificar páginas ativas/arquivadas com tabs (v1.3.1)

Substitui rotas separadas de inativos/arquivados por tabs inline em
Cartões, Contas e Anotações, seguindo o padrão já usado em Categorias.
Remove sub-links da sidebar e padroniza nomenclatura para "Arquivados".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-02-06 13:20:15 +00:00
parent 6f5c41a4cf
commit 4152a27f4d
15 changed files with 330 additions and 278 deletions

View File

@@ -5,6 +5,15 @@ Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
## [1.3.1] - 2026-02-06
### Alterado
- Unificadas páginas de itens ativos e arquivados em Cartões, Contas e Anotações com sistema de tabs (padrão Categorias)
- Removidas rotas separadas `/cartoes/inativos`, `/contas/inativos` e `/anotacoes/arquivadas`
- Removidos sub-links de inativos/arquivados da sidebar
- Padronizada nomenclatura para "Arquivados"/"Arquivadas" em todas as entidades
## [1.3.0] - 2026-02-06
### Adicionado

View File

@@ -1,14 +0,0 @@
import { NotesPage } from "@/components/anotacoes/notes-page";
import { getUserId } from "@/lib/auth/server";
import { fetchArquivadasForUser } from "../data";
export default async function ArquivadasPage() {
const userId = await getUserId();
const notes = await fetchArquivadasForUser(userId);
return (
<main className="flex flex-col items-start gap-6">
<NotesPage notes={notes} isArquivadas={true} />
</main>
);
}

View File

@@ -52,6 +52,17 @@ export async function fetchNotesForUser(userId: string): Promise<NoteData[]> {
});
}
export async function fetchAllNotesForUser(
userId: string,
): Promise<{ activeNotes: NoteData[]; archivedNotes: NoteData[] }> {
const [activeNotes, archivedNotes] = await Promise.all([
fetchNotesForUser(userId),
fetchArquivadasForUser(userId),
]);
return { activeNotes, archivedNotes };
}
export async function fetchArquivadasForUser(
userId: string,
): Promise<NoteData[]> {

View File

@@ -1,14 +1,14 @@
import { NotesPage } from "@/components/anotacoes/notes-page";
import { getUserId } from "@/lib/auth/server";
import { fetchNotesForUser } from "./data";
import { fetchAllNotesForUser } from "./data";
export default async function Page() {
const userId = await getUserId();
const notes = await fetchNotesForUser(userId);
const { activeNotes, archivedNotes } = await fetchAllNotesForUser(userId);
return (
<main className="flex flex-col items-start gap-6">
<NotesPage notes={notes} />
<NotesPage notes={activeNotes} archivedNotes={archivedNotes} />
</main>
);
}

View File

@@ -211,3 +211,22 @@ export async function fetchInativosForUser(userId: string): Promise<{
return { cards, accounts, logoOptions };
}
export async function fetchAllCardsForUser(userId: string): Promise<{
activeCards: CardData[];
archivedCards: CardData[];
accounts: AccountSimple[];
logoOptions: LogoOption[];
}> {
const [activeData, archivedData] = await Promise.all([
fetchCardsForUser(userId),
fetchInativosForUser(userId),
]);
return {
activeCards: activeData.cards,
archivedCards: archivedData.cards,
accounts: activeData.accounts,
logoOptions: activeData.logoOptions,
};
}

View File

@@ -1,19 +0,0 @@
import { CardsPage } from "@/components/cartoes/cards-page";
import { getUserId } from "@/lib/auth/server";
import { fetchInativosForUser } from "../data";
export default async function InativosPage() {
const userId = await getUserId();
const { cards, accounts, logoOptions } = await fetchInativosForUser(userId);
return (
<main className="flex flex-col items-start gap-6">
<CardsPage
cards={cards}
accounts={accounts}
logoOptions={logoOptions}
isInativos={true}
/>
</main>
);
}

View File

@@ -1,14 +1,20 @@
import { CardsPage } from "@/components/cartoes/cards-page";
import { getUserId } from "@/lib/auth/server";
import { fetchCardsForUser } from "./data";
import { fetchAllCardsForUser } from "./data";
export default async function Page() {
const userId = await getUserId();
const { cards, accounts, logoOptions } = await fetchCardsForUser(userId);
const { activeCards, archivedCards, accounts, logoOptions } =
await fetchAllCardsForUser(userId);
return (
<main className="flex flex-col items-start gap-6">
<CardsPage cards={cards} accounts={accounts} logoOptions={logoOptions} />
<CardsPage
cards={activeCards}
archivedCards={archivedCards}
accounts={accounts}
logoOptions={logoOptions}
/>
</main>
);
}

View File

@@ -169,3 +169,20 @@ export async function fetchInativosForUser(
return { accounts, logoOptions };
}
export async function fetchAllAccountsForUser(userId: string): Promise<{
activeAccounts: AccountData[];
archivedAccounts: AccountData[];
logoOptions: LogoOption[];
}> {
const [activeData, archivedData] = await Promise.all([
fetchAccountsForUser(userId),
fetchInativosForUser(userId),
]);
return {
activeAccounts: activeData.accounts,
archivedAccounts: archivedData.accounts,
logoOptions: activeData.logoOptions,
};
}

View File

@@ -1,18 +0,0 @@
import { AccountsPage } from "@/components/contas/accounts-page";
import { getUserId } from "@/lib/auth/server";
import { fetchInativosForUser } from "../data";
export default async function InativosPage() {
const userId = await getUserId();
const { accounts, logoOptions } = await fetchInativosForUser(userId);
return (
<main className="flex flex-col items-start gap-6">
<AccountsPage
accounts={accounts}
logoOptions={logoOptions}
isInativos={true}
/>
</main>
);
}

View File

@@ -1,14 +1,19 @@
import { AccountsPage } from "@/components/contas/accounts-page";
import { getUserId } from "@/lib/auth/server";
import { fetchAccountsForUser } from "./data";
import { fetchAllAccountsForUser } from "./data";
export default async function Page() {
const userId = await getUserId();
const { accounts, logoOptions } = await fetchAccountsForUser(userId);
const { activeAccounts, archivedAccounts, logoOptions } =
await fetchAllAccountsForUser(userId);
return (
<main className="flex flex-col items-start gap-6">
<AccountsPage accounts={accounts} logoOptions={logoOptions} />
<AccountsPage
accounts={activeAccounts}
archivedAccounts={archivedAccounts}
logoOptions={logoOptions}
/>
</main>
);
}

View File

@@ -10,6 +10,7 @@ import {
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
import { EmptyState } from "@/components/empty-state";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "../ui/card";
import { NoteCard } from "./note-card";
import { NoteDetailsDialog } from "./note-details-dialog";
@@ -18,10 +19,11 @@ import type { Note } from "./types";
interface NotesPageProps {
notes: Note[];
isArquivadas?: boolean;
archivedNotes: Note[];
}
export function NotesPage({ notes, isArquivadas = false }: NotesPageProps) {
export function NotesPage({ notes, archivedNotes }: NotesPageProps) {
const [activeTab, setActiveTab] = useState("ativas");
const [createOpen, setCreateOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
const [noteToEdit, setNoteToEdit] = useState<Note | null>(null);
@@ -32,15 +34,23 @@ export function NotesPage({ notes, isArquivadas = false }: NotesPageProps) {
const [arquivarOpen, setArquivarOpen] = useState(false);
const [noteToArquivar, setNoteToArquivar] = useState<Note | null>(null);
const sortedNotes = useMemo(
() =>
[...notes].sort(
const sortNotes = useCallback(
(list: Note[]) =>
[...list].sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
),
[notes],
[],
);
const sortedNotes = useMemo(() => sortNotes(notes), [notes, sortNotes]);
const sortedArchivedNotes = useMemo(
() => sortNotes(archivedNotes),
[archivedNotes, sortNotes],
);
const isArquivadas = activeTab === "arquivadas";
const handleCreateOpenChange = useCallback((open: boolean) => {
setCreateOpen(open);
}, []);
@@ -146,56 +156,75 @@ export function NotesPage({ notes, isArquivadas = false }: NotesPageProps) {
? "Desarquivar anotação?"
: "Arquivar anotação?";
const renderNoteList = (list: Note[], isArchived: boolean) => {
if (list.length === 0) {
return (
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
<EmptyState
media={<RiTodoLine className="size-6 text-primary" />}
title={
isArchived
? "Nenhuma anotação arquivada"
: "Nenhuma anotação registrada"
}
description={
isArchived
? "As anotações arquivadas aparecerão aqui."
: "Crie anotações personalizadas para acompanhar lembretes, decisões ou observações financeiras importantes."
}
/>
</Card>
);
}
return (
<div className="flex flex-wrap gap-4">
{list.map((note) => (
<NoteCard
key={note.id}
note={note}
onEdit={handleEditRequest}
onDetails={handleDetailsRequest}
onRemove={handleRemoveRequest}
onArquivar={handleArquivarRequest}
isArquivadas={isArchived}
/>
))}
</div>
);
};
return (
<>
<div className="flex w-full flex-col gap-6">
{!isArquivadas && (
<div className="flex justify-start">
<NoteDialog
mode="create"
open={createOpen}
onOpenChange={handleCreateOpenChange}
trigger={
<Button>
<RiAddCircleLine className="size-4" />
Nova anotação
</Button>
}
/>
</div>
)}
<div className="flex justify-start">
<NoteDialog
mode="create"
open={createOpen}
onOpenChange={handleCreateOpenChange}
trigger={
<Button>
<RiAddCircleLine className="size-4" />
Nova anotação
</Button>
}
/>
</div>
{sortedNotes.length === 0 ? (
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
<EmptyState
media={<RiTodoLine className="size-6 text-primary" />}
title={
isArquivadas
? "Nenhuma anotação arquivada"
: "Nenhuma anotação registrada"
}
description={
isArquivadas
? "As anotações arquivadas aparecerão aqui."
: "Crie anotações personalizadas para acompanhar lembretes, decisões ou observações financeiras importantes."
}
/>
</Card>
) : (
<div className="flex flex-wrap gap-4">
{sortedNotes.map((note) => (
<NoteCard
key={note.id}
note={note}
onEdit={handleEditRequest}
onDetails={handleDetailsRequest}
onRemove={handleRemoveRequest}
onArquivar={handleArquivarRequest}
isArquivadas={isArquivadas}
/>
))}
</div>
)}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList>
<TabsTrigger value="ativas">Ativas</TabsTrigger>
<TabsTrigger value="arquivadas">Arquivadas</TabsTrigger>
</TabsList>
<TabsContent value="ativas" className="mt-4">
{renderNoteList(sortedNotes, false)}
</TabsContent>
<TabsContent value="arquivadas" className="mt-4">
{renderNoteList(sortedArchivedNotes, true)}
</TabsContent>
</Tabs>
</div>
<NoteDialog

View File

@@ -9,6 +9,7 @@ import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
import { EmptyState } from "@/components/empty-state";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CardDialog } from "./card-dialog";
import { CardItem } from "./card-item";
@@ -19,39 +20,36 @@ type AccountOption = {
interface CardsPageProps {
cards: Card[];
archivedCards: Card[];
accounts: AccountOption[];
logoOptions: string[];
isInativos?: boolean;
}
export function CardsPage({
cards,
archivedCards,
accounts,
logoOptions,
isInativos = false,
}: CardsPageProps) {
const router = useRouter();
const [activeTab, setActiveTab] = useState("ativos");
const [editOpen, setEditOpen] = useState(false);
const [selectedCard, setSelectedCard] = useState<Card | null>(null);
const [removeOpen, setRemoveOpen] = useState(false);
const [cardToRemove, setCardToRemove] = useState<Card | null>(null);
const hasCards = cards.length > 0;
const sortCards = useCallback(
(list: Card[]) =>
[...list].sort((a, b) =>
a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" }),
),
[],
);
const orderedCards = useMemo(
() =>
[...cards].sort((a, b) => {
// Coloca inativos no final
const aIsInactive = a.status?.toLowerCase() === "inativo";
const bIsInactive = b.status?.toLowerCase() === "inativo";
if (aIsInactive && !bIsInactive) return 1;
if (!aIsInactive && bIsInactive) return -1;
// Mesma ordem alfabética dentro de cada grupo
return a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" });
}),
[cards],
const orderedCards = useMemo(() => sortCards(cards), [cards, sortCards]);
const orderedArchivedCards = useMemo(
() => sortCards(archivedCards),
[archivedCards, sortCards],
);
const handleEdit = useCallback((card: Card) => {
@@ -105,64 +103,83 @@ export function CardsPage({
? `Remover cartão "${cardToRemove.name}"?`
: "Remover cartão?";
const renderCardList = (list: Card[], isArchived: boolean) => {
if (list.length === 0) {
return (
<Card className="flex w-full items-center justify-center py-12">
<EmptyState
media={<RiBankCard2Line className="size-6 text-primary" />}
title={
isArchived
? "Nenhum cartão arquivado"
: "Nenhum cartão cadastrado"
}
description={
isArchived
? "Os cartões arquivados aparecerão aqui."
: "Adicione seu primeiro cartão para acompanhar limites e faturas com mais controle."
}
/>
</Card>
);
}
return (
<div className="flex flex-wrap gap-4">
{list.map((card) => (
<CardItem
key={card.id}
name={card.name}
brand={card.brand}
status={card.status}
closingDay={card.closingDay}
dueDay={card.dueDay}
limit={card.limit}
limitInUse={card.limitInUse ?? null}
limitAvailable={card.limitAvailable ?? card.limit ?? null}
contaName={card.contaName}
logo={card.logo}
note={card.note}
onEdit={() => handleEdit(card)}
onInvoice={() => handleInvoice(card)}
onRemove={() => handleRemoveRequest(card)}
/>
))}
</div>
);
};
return (
<>
<div className="flex w-full flex-col gap-6">
{!isInativos && (
<div className="flex justify-start">
<CardDialog
mode="create"
accounts={accounts}
logoOptions={logoOptions}
trigger={
<Button>
<RiAddCircleLine className="size-4" />
Novo cartão
</Button>
}
/>
</div>
)}
<div className="flex justify-start">
<CardDialog
mode="create"
accounts={accounts}
logoOptions={logoOptions}
trigger={
<Button>
<RiAddCircleLine className="size-4" />
Novo cartão
</Button>
}
/>
</div>
{hasCards ? (
<div className="flex flex-wrap gap-4">
{orderedCards.map((card) => (
<CardItem
key={card.id}
name={card.name}
brand={card.brand}
status={card.status}
closingDay={card.closingDay}
dueDay={card.dueDay}
limit={card.limit}
limitInUse={card.limitInUse ?? null}
limitAvailable={card.limitAvailable ?? card.limit ?? null}
contaName={card.contaName}
logo={card.logo}
note={card.note}
onEdit={() => handleEdit(card)}
onInvoice={() => handleInvoice(card)}
onRemove={() => handleRemoveRequest(card)}
/>
))}
</div>
) : (
<Card className="flex w-full items-center justify-center py-12">
<EmptyState
media={<RiBankCard2Line className="size-6 text-primary" />}
title={
isInativos
? "Nenhum cartão inativo"
: "Nenhum cartão cadastrado"
}
description={
isInativos
? "Os cartões inativos aparecerão aqui."
: "Adicione seu primeiro cartão para acompanhar limites e faturas com mais controle."
}
/>
</Card>
)}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList>
<TabsTrigger value="ativos">Ativos</TabsTrigger>
<TabsTrigger value="arquivados">Arquivados</TabsTrigger>
</TabsList>
<TabsContent value="ativos" className="mt-4">
{renderCardList(orderedCards, false)}
</TabsContent>
<TabsContent value="arquivados" className="mt-4">
{renderCardList(orderedArchivedCards, true)}
</TabsContent>
</Tabs>
</div>
<CardDialog

View File

@@ -10,6 +10,7 @@ import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
import { AccountCard } from "@/components/contas/account-card";
import { EmptyState } from "@/components/empty-state";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getCurrentPeriod } from "@/lib/utils/period";
import { Card } from "../ui/card";
import { AccountDialog } from "./account-dialog";
@@ -18,8 +19,8 @@ import type { Account } from "./types";
interface AccountsPageProps {
accounts: Account[];
archivedAccounts: Account[];
logoOptions: string[];
isInativos?: boolean;
}
const resolveLogoSrc = (logo: string | null) => {
@@ -33,10 +34,11 @@ const resolveLogoSrc = (logo: string | null) => {
export function AccountsPage({
accounts,
archivedAccounts,
logoOptions,
isInativos = false,
}: AccountsPageProps) {
const router = useRouter();
const [activeTab, setActiveTab] = useState("ativos");
const [editOpen, setEditOpen] = useState(false);
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
const [removeOpen, setRemoveOpen] = useState(false);
@@ -45,19 +47,13 @@ export function AccountsPage({
const [transferFromAccount, setTransferFromAccount] =
useState<Account | null>(null);
const hasAccounts = accounts.length > 0;
const sortAccounts = (list: Account[]) =>
[...list].sort((a, b) =>
a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" }),
);
const orderedAccounts = [...accounts].sort((a, b) => {
// Coloca inativas no final
const aIsInactive = a.status?.toLowerCase() === "inativa";
const bIsInactive = b.status?.toLowerCase() === "inativa";
if (aIsInactive && !bIsInactive) return 1;
if (!aIsInactive && bIsInactive) return -1;
// Mesma ordem alfabética dentro de cada grupo
return a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" });
});
const orderedAccounts = sortAccounts(accounts);
const orderedArchivedAccounts = sortAccounts(archivedAccounts);
const handleEdit = (account: Account) => {
setSelectedAccount(account);
@@ -115,6 +111,67 @@ export function AccountsPage({
? `Remover conta "${accountToRemove.name}"?`
: "Remover conta?";
const renderAccountList = (list: Account[], isArchived: boolean) => {
if (list.length === 0) {
return (
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
<EmptyState
media={<RiBankLine className="size-6 text-primary" />}
title={
isArchived
? "Nenhuma conta arquivada"
: "Nenhuma conta cadastrada"
}
description={
isArchived
? "As contas arquivadas aparecerão aqui."
: "Cadastre sua primeira conta para começar a organizar os lançamentos."
}
/>
</Card>
);
}
return (
<div className="flex flex-wrap gap-4">
{list.map((account) => {
const logoSrc = resolveLogoSrc(account.logo);
return (
<AccountCard
key={account.id}
accountName={account.name}
accountType={`${account.accountType}`}
balance={account.balance ?? account.initialBalance ?? 0}
status={account.status}
excludeFromBalance={account.excludeFromBalance}
excludeInitialBalanceFromIncome={
account.excludeInitialBalanceFromIncome
}
icon={
logoSrc ? (
<Image
src={logoSrc}
alt={`Logo da conta ${account.name}`}
width={42}
height={42}
className="rounded-lg"
/>
) : undefined
}
onEdit={() => handleEdit(account)}
onRemove={() => handleRemoveRequest(account)}
onTransfer={() => handleTransferRequest(account)}
onViewStatement={() =>
router.push(`/contas/${account.id}/extrato`)
}
/>
);
})}
</div>
);
};
return (
<>
<div className="flex w-full flex-col gap-6">
@@ -131,60 +188,20 @@ export function AccountsPage({
/>
</div>
{hasAccounts ? (
<div className="flex flex-wrap gap-4">
{orderedAccounts.map((account) => {
const logoSrc = resolveLogoSrc(account.logo);
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList>
<TabsTrigger value="ativos">Ativas</TabsTrigger>
<TabsTrigger value="arquivados">Arquivadas</TabsTrigger>
</TabsList>
return (
<AccountCard
key={account.id}
accountName={account.name}
accountType={`${account.accountType}`}
balance={account.balance ?? account.initialBalance ?? 0}
status={account.status}
excludeFromBalance={account.excludeFromBalance}
excludeInitialBalanceFromIncome={
account.excludeInitialBalanceFromIncome
}
icon={
logoSrc ? (
<Image
src={logoSrc}
alt={`Logo da conta ${account.name}`}
width={42}
height={42}
className="rounded-lg"
/>
) : undefined
}
onEdit={() => handleEdit(account)}
onRemove={() => handleRemoveRequest(account)}
onTransfer={() => handleTransferRequest(account)}
onViewStatement={() =>
router.push(`/contas/${account.id}/extrato`)
}
/>
);
})}
</div>
) : (
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
<EmptyState
media={<RiBankLine className="size-6 text-primary" />}
title={
isInativos
? "Nenhuma conta inativa"
: "Nenhuma conta cadastrada"
}
description={
isInativos
? "Não há contas inativas no momento."
: "Cadastre sua primeira conta para começar a organizar os lançamentos."
}
/>
</Card>
)}
<TabsContent value="ativos" className="mt-4">
{renderAccountList(orderedAccounts, false)}
</TabsContent>
<TabsContent value="arquivados" className="mt-4">
{renderAccountList(orderedArchivedAccounts, true)}
</TabsContent>
</Tabs>
</div>
<AccountDialog

View File

@@ -1,17 +1,14 @@
import {
type RemixiconComponentType,
RiArchiveLine,
RiArrowLeftRightLine,
RiBankCard2Line,
RiBankLine,
RiCalendarEventLine,
RiDashboardLine,
RiEyeOffLine,
RiFileChartLine,
RiFundsLine,
RiGroupLine,
RiInboxLine,
RiNoCreditCardLine,
RiPriceTag3Line,
RiSettings2Line,
RiSparklingLine,
@@ -116,27 +113,11 @@ export function createSidebarNavData(
title: "Cartões",
url: "/cartoes",
icon: RiBankCard2Line,
items: [
{
title: "Inativos",
url: "/cartoes/inativos",
key: "cartoes-inativos",
icon: RiNoCreditCardLine,
},
],
},
{
title: "Contas",
url: "/contas",
icon: RiBankLine,
items: [
{
title: "Inativas",
url: "/contas/inativos",
key: "contas-inativos",
icon: RiEyeOffLine,
},
],
},
{
title: "Orçamentos",
@@ -163,14 +144,6 @@ export function createSidebarNavData(
title: "Anotações",
url: "/anotacoes",
icon: RiTodoLine,
items: [
{
title: "Arquivadas",
url: "/anotacoes/arquivadas",
key: "anotacoes-arquivadas",
icon: RiArchiveLine,
},
],
},
],
},

View File

@@ -1,6 +1,6 @@
{
"name": "opensheets",
"version": "1.3.0",
"version": "1.3.1",
"private": true,
"scripts": {
"dev": "next dev --turbopack",