forked from git.gladyson/openmonetis
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:
@@ -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/),
|
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/).
|
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
|
## [1.3.0] - 2026-02-06
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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(
|
export async function fetchArquivadasForUser(
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<NoteData[]> {
|
): Promise<NoteData[]> {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { NotesPage } from "@/components/anotacoes/notes-page";
|
import { NotesPage } from "@/components/anotacoes/notes-page";
|
||||||
import { getUserId } from "@/lib/auth/server";
|
import { getUserId } from "@/lib/auth/server";
|
||||||
import { fetchNotesForUser } from "./data";
|
import { fetchAllNotesForUser } from "./data";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const notes = await fetchNotesForUser(userId);
|
const { activeNotes, archivedNotes } = await fetchAllNotesForUser(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col items-start gap-6">
|
||||||
<NotesPage notes={notes} />
|
<NotesPage notes={activeNotes} archivedNotes={archivedNotes} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,3 +211,22 @@ export async function fetchInativosForUser(userId: string): Promise<{
|
|||||||
|
|
||||||
return { cards, accounts, logoOptions };
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
import { CardsPage } from "@/components/cartoes/cards-page";
|
import { CardsPage } from "@/components/cartoes/cards-page";
|
||||||
import { getUserId } from "@/lib/auth/server";
|
import { getUserId } from "@/lib/auth/server";
|
||||||
import { fetchCardsForUser } from "./data";
|
import { fetchAllCardsForUser } from "./data";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const { cards, accounts, logoOptions } = await fetchCardsForUser(userId);
|
const { activeCards, archivedCards, accounts, logoOptions } =
|
||||||
|
await fetchAllCardsForUser(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<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>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,3 +169,20 @@ export async function fetchInativosForUser(
|
|||||||
|
|
||||||
return { accounts, logoOptions };
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
import { AccountsPage } from "@/components/contas/accounts-page";
|
import { AccountsPage } from "@/components/contas/accounts-page";
|
||||||
import { getUserId } from "@/lib/auth/server";
|
import { getUserId } from "@/lib/auth/server";
|
||||||
import { fetchAccountsForUser } from "./data";
|
import { fetchAllAccountsForUser } from "./data";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const userId = await getUserId();
|
const userId = await getUserId();
|
||||||
const { accounts, logoOptions } = await fetchAccountsForUser(userId);
|
const { activeAccounts, archivedAccounts, logoOptions } =
|
||||||
|
await fetchAllAccountsForUser(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-start gap-6">
|
<main className="flex flex-col items-start gap-6">
|
||||||
<AccountsPage accounts={accounts} logoOptions={logoOptions} />
|
<AccountsPage
|
||||||
|
accounts={activeAccounts}
|
||||||
|
archivedAccounts={archivedAccounts}
|
||||||
|
logoOptions={logoOptions}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
import { NoteCard } from "./note-card";
|
import { NoteCard } from "./note-card";
|
||||||
import { NoteDetailsDialog } from "./note-details-dialog";
|
import { NoteDetailsDialog } from "./note-details-dialog";
|
||||||
@@ -18,10 +19,11 @@ import type { Note } from "./types";
|
|||||||
|
|
||||||
interface NotesPageProps {
|
interface NotesPageProps {
|
||||||
notes: Note[];
|
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 [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [noteToEdit, setNoteToEdit] = useState<Note | null>(null);
|
const [noteToEdit, setNoteToEdit] = useState<Note | null>(null);
|
||||||
@@ -32,15 +34,23 @@ export function NotesPage({ notes, isArquivadas = false }: NotesPageProps) {
|
|||||||
const [arquivarOpen, setArquivarOpen] = useState(false);
|
const [arquivarOpen, setArquivarOpen] = useState(false);
|
||||||
const [noteToArquivar, setNoteToArquivar] = useState<Note | null>(null);
|
const [noteToArquivar, setNoteToArquivar] = useState<Note | null>(null);
|
||||||
|
|
||||||
const sortedNotes = useMemo(
|
const sortNotes = useCallback(
|
||||||
() =>
|
(list: Note[]) =>
|
||||||
[...notes].sort(
|
[...list].sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
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) => {
|
const handleCreateOpenChange = useCallback((open: boolean) => {
|
||||||
setCreateOpen(open);
|
setCreateOpen(open);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -146,56 +156,75 @@ export function NotesPage({ notes, isArquivadas = false }: NotesPageProps) {
|
|||||||
? "Desarquivar anotação?"
|
? "Desarquivar anotação?"
|
||||||
: "Arquivar 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
{!isArquivadas && (
|
<div className="flex justify-start">
|
||||||
<div className="flex justify-start">
|
<NoteDialog
|
||||||
<NoteDialog
|
mode="create"
|
||||||
mode="create"
|
open={createOpen}
|
||||||
open={createOpen}
|
onOpenChange={handleCreateOpenChange}
|
||||||
onOpenChange={handleCreateOpenChange}
|
trigger={
|
||||||
trigger={
|
<Button>
|
||||||
<Button>
|
<RiAddCircleLine className="size-4" />
|
||||||
<RiAddCircleLine className="size-4" />
|
Nova anotação
|
||||||
Nova anotação
|
</Button>
|
||||||
</Button>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sortedNotes.length === 0 ? (
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
|
<TabsList>
|
||||||
<EmptyState
|
<TabsTrigger value="ativas">Ativas</TabsTrigger>
|
||||||
media={<RiTodoLine className="size-6 text-primary" />}
|
<TabsTrigger value="arquivadas">Arquivadas</TabsTrigger>
|
||||||
title={
|
</TabsList>
|
||||||
isArquivadas
|
|
||||||
? "Nenhuma anotação arquivada"
|
<TabsContent value="ativas" className="mt-4">
|
||||||
: "Nenhuma anotação registrada"
|
{renderNoteList(sortedNotes, false)}
|
||||||
}
|
</TabsContent>
|
||||||
description={
|
|
||||||
isArquivadas
|
<TabsContent value="arquivadas" className="mt-4">
|
||||||
? "As anotações arquivadas aparecerão aqui."
|
{renderNoteList(sortedArchivedNotes, true)}
|
||||||
: "Crie anotações personalizadas para acompanhar lembretes, decisões ou observações financeiras importantes."
|
</TabsContent>
|
||||||
}
|
</Tabs>
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NoteDialog
|
<NoteDialog
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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 { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { CardDialog } from "./card-dialog";
|
import { CardDialog } from "./card-dialog";
|
||||||
import { CardItem } from "./card-item";
|
import { CardItem } from "./card-item";
|
||||||
|
|
||||||
@@ -19,39 +20,36 @@ type AccountOption = {
|
|||||||
|
|
||||||
interface CardsPageProps {
|
interface CardsPageProps {
|
||||||
cards: Card[];
|
cards: Card[];
|
||||||
|
archivedCards: Card[];
|
||||||
accounts: AccountOption[];
|
accounts: AccountOption[];
|
||||||
logoOptions: string[];
|
logoOptions: string[];
|
||||||
isInativos?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CardsPage({
|
export function CardsPage({
|
||||||
cards,
|
cards,
|
||||||
|
archivedCards,
|
||||||
accounts,
|
accounts,
|
||||||
logoOptions,
|
logoOptions,
|
||||||
isInativos = false,
|
|
||||||
}: CardsPageProps) {
|
}: CardsPageProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [activeTab, setActiveTab] = useState("ativos");
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [selectedCard, setSelectedCard] = useState<Card | null>(null);
|
const [selectedCard, setSelectedCard] = useState<Card | null>(null);
|
||||||
const [removeOpen, setRemoveOpen] = useState(false);
|
const [removeOpen, setRemoveOpen] = useState(false);
|
||||||
const [cardToRemove, setCardToRemove] = useState<Card | null>(null);
|
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(
|
const orderedCards = useMemo(() => sortCards(cards), [cards, sortCards]);
|
||||||
() =>
|
const orderedArchivedCards = useMemo(
|
||||||
[...cards].sort((a, b) => {
|
() => sortCards(archivedCards),
|
||||||
// Coloca inativos no final
|
[archivedCards, sortCards],
|
||||||
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 handleEdit = useCallback((card: Card) => {
|
const handleEdit = useCallback((card: Card) => {
|
||||||
@@ -105,64 +103,83 @@ export function CardsPage({
|
|||||||
? `Remover cartão "${cardToRemove.name}"?`
|
? `Remover cartão "${cardToRemove.name}"?`
|
||||||
: "Remover cartão?";
|
: "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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
{!isInativos && (
|
<div className="flex justify-start">
|
||||||
<div className="flex justify-start">
|
<CardDialog
|
||||||
<CardDialog
|
mode="create"
|
||||||
mode="create"
|
accounts={accounts}
|
||||||
accounts={accounts}
|
logoOptions={logoOptions}
|
||||||
logoOptions={logoOptions}
|
trigger={
|
||||||
trigger={
|
<Button>
|
||||||
<Button>
|
<RiAddCircleLine className="size-4" />
|
||||||
<RiAddCircleLine className="size-4" />
|
Novo cartão
|
||||||
Novo cartão
|
</Button>
|
||||||
</Button>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasCards ? (
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<div className="flex flex-wrap gap-4">
|
<TabsList>
|
||||||
{orderedCards.map((card) => (
|
<TabsTrigger value="ativos">Ativos</TabsTrigger>
|
||||||
<CardItem
|
<TabsTrigger value="arquivados">Arquivados</TabsTrigger>
|
||||||
key={card.id}
|
</TabsList>
|
||||||
name={card.name}
|
|
||||||
brand={card.brand}
|
<TabsContent value="ativos" className="mt-4">
|
||||||
status={card.status}
|
{renderCardList(orderedCards, false)}
|
||||||
closingDay={card.closingDay}
|
</TabsContent>
|
||||||
dueDay={card.dueDay}
|
|
||||||
limit={card.limit}
|
<TabsContent value="arquivados" className="mt-4">
|
||||||
limitInUse={card.limitInUse ?? null}
|
{renderCardList(orderedArchivedCards, true)}
|
||||||
limitAvailable={card.limitAvailable ?? card.limit ?? null}
|
</TabsContent>
|
||||||
contaName={card.contaName}
|
</Tabs>
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardDialog
|
<CardDialog
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
|||||||
import { AccountCard } from "@/components/contas/account-card";
|
import { AccountCard } from "@/components/contas/account-card";
|
||||||
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { getCurrentPeriod } from "@/lib/utils/period";
|
import { getCurrentPeriod } from "@/lib/utils/period";
|
||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
import { AccountDialog } from "./account-dialog";
|
import { AccountDialog } from "./account-dialog";
|
||||||
@@ -18,8 +19,8 @@ import type { Account } from "./types";
|
|||||||
|
|
||||||
interface AccountsPageProps {
|
interface AccountsPageProps {
|
||||||
accounts: Account[];
|
accounts: Account[];
|
||||||
|
archivedAccounts: Account[];
|
||||||
logoOptions: string[];
|
logoOptions: string[];
|
||||||
isInativos?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveLogoSrc = (logo: string | null) => {
|
const resolveLogoSrc = (logo: string | null) => {
|
||||||
@@ -33,10 +34,11 @@ const resolveLogoSrc = (logo: string | null) => {
|
|||||||
|
|
||||||
export function AccountsPage({
|
export function AccountsPage({
|
||||||
accounts,
|
accounts,
|
||||||
|
archivedAccounts,
|
||||||
logoOptions,
|
logoOptions,
|
||||||
isInativos = false,
|
|
||||||
}: AccountsPageProps) {
|
}: AccountsPageProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [activeTab, setActiveTab] = useState("ativos");
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
||||||
const [removeOpen, setRemoveOpen] = useState(false);
|
const [removeOpen, setRemoveOpen] = useState(false);
|
||||||
@@ -45,19 +47,13 @@ export function AccountsPage({
|
|||||||
const [transferFromAccount, setTransferFromAccount] =
|
const [transferFromAccount, setTransferFromAccount] =
|
||||||
useState<Account | null>(null);
|
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) => {
|
const orderedAccounts = sortAccounts(accounts);
|
||||||
// Coloca inativas no final
|
const orderedArchivedAccounts = sortAccounts(archivedAccounts);
|
||||||
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 handleEdit = (account: Account) => {
|
const handleEdit = (account: Account) => {
|
||||||
setSelectedAccount(account);
|
setSelectedAccount(account);
|
||||||
@@ -115,6 +111,67 @@ export function AccountsPage({
|
|||||||
? `Remover conta "${accountToRemove.name}"?`
|
? `Remover conta "${accountToRemove.name}"?`
|
||||||
: "Remover conta?";
|
: "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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
@@ -131,60 +188,20 @@ export function AccountsPage({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasAccounts ? (
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<div className="flex flex-wrap gap-4">
|
<TabsList>
|
||||||
{orderedAccounts.map((account) => {
|
<TabsTrigger value="ativos">Ativas</TabsTrigger>
|
||||||
const logoSrc = resolveLogoSrc(account.logo);
|
<TabsTrigger value="arquivados">Arquivadas</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
return (
|
<TabsContent value="ativos" className="mt-4">
|
||||||
<AccountCard
|
{renderAccountList(orderedAccounts, false)}
|
||||||
key={account.id}
|
</TabsContent>
|
||||||
accountName={account.name}
|
|
||||||
accountType={`${account.accountType}`}
|
<TabsContent value="arquivados" className="mt-4">
|
||||||
balance={account.balance ?? account.initialBalance ?? 0}
|
{renderAccountList(orderedArchivedAccounts, true)}
|
||||||
status={account.status}
|
</TabsContent>
|
||||||
excludeFromBalance={account.excludeFromBalance}
|
</Tabs>
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AccountDialog
|
<AccountDialog
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
type RemixiconComponentType,
|
type RemixiconComponentType,
|
||||||
RiArchiveLine,
|
|
||||||
RiArrowLeftRightLine,
|
RiArrowLeftRightLine,
|
||||||
RiBankCard2Line,
|
RiBankCard2Line,
|
||||||
RiBankLine,
|
RiBankLine,
|
||||||
RiCalendarEventLine,
|
RiCalendarEventLine,
|
||||||
RiDashboardLine,
|
RiDashboardLine,
|
||||||
RiEyeOffLine,
|
|
||||||
RiFileChartLine,
|
RiFileChartLine,
|
||||||
RiFundsLine,
|
RiFundsLine,
|
||||||
RiGroupLine,
|
RiGroupLine,
|
||||||
RiInboxLine,
|
RiInboxLine,
|
||||||
RiNoCreditCardLine,
|
|
||||||
RiPriceTag3Line,
|
RiPriceTag3Line,
|
||||||
RiSettings2Line,
|
RiSettings2Line,
|
||||||
RiSparklingLine,
|
RiSparklingLine,
|
||||||
@@ -116,27 +113,11 @@ export function createSidebarNavData(
|
|||||||
title: "Cartões",
|
title: "Cartões",
|
||||||
url: "/cartoes",
|
url: "/cartoes",
|
||||||
icon: RiBankCard2Line,
|
icon: RiBankCard2Line,
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Inativos",
|
|
||||||
url: "/cartoes/inativos",
|
|
||||||
key: "cartoes-inativos",
|
|
||||||
icon: RiNoCreditCardLine,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Contas",
|
title: "Contas",
|
||||||
url: "/contas",
|
url: "/contas",
|
||||||
icon: RiBankLine,
|
icon: RiBankLine,
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Inativas",
|
|
||||||
url: "/contas/inativos",
|
|
||||||
key: "contas-inativos",
|
|
||||||
icon: RiEyeOffLine,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Orçamentos",
|
title: "Orçamentos",
|
||||||
@@ -163,14 +144,6 @@ export function createSidebarNavData(
|
|||||||
title: "Anotações",
|
title: "Anotações",
|
||||||
url: "/anotacoes",
|
url: "/anotacoes",
|
||||||
icon: RiTodoLine,
|
icon: RiTodoLine,
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Arquivadas",
|
|
||||||
url: "/anotacoes/arquivadas",
|
|
||||||
key: "anotacoes-arquivadas",
|
|
||||||
icon: RiArchiveLine,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "opensheets",
|
"name": "opensheets",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
|
|||||||
Reference in New Issue
Block a user