From 11f85e4b285392735f2c815a88c5d53e97d60736 Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Fri, 30 Jan 2026 02:23:32 +0000 Subject: [PATCH] =?UTF-8?q?refactor(ui):=20melhorar=20layouts=20da=20aba?= =?UTF-8?q?=20Companion=20e=20p=C3=A1gina=20de=20cart=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Criar componente CompanionTab com layout reorganizado - Simplificar ApiTokensForm com lista de dispositivos mais compacta - Redesenhar página de relatórios de cartões com layout horizontal - Atualizar CardsOverview com stats em cards separados e lista compacta - Simplificar CardUsageChart removendo seletor de período (fixo 12 meses) - Criar CardInvoiceStatus com timeline minimalista - Atualizar skeletons para refletir novos layouts Co-Authored-By: Claude Opus 4.5 --- app/(dashboard)/ajustes/page.tsx | 20 +- .../relatorios/cartoes/loading.tsx | 145 +++++------ app/(dashboard)/relatorios/cartoes/page.tsx | 63 +++-- app/(dashboard)/top-estabelecimentos/page.tsx | 6 +- app/globals.css | 4 +- components/ajustes/api-tokens-form.tsx | 109 ++++---- components/ajustes/companion-tab.tsx | 98 +++++++ components/pre-lancamentos/inbox-page.tsx | 2 +- .../cartoes/card-invoice-status.tsx | 113 +++++---- .../relatorios/cartoes/card-top-expenses.tsx | 2 +- .../relatorios/cartoes/card-usage-chart.tsx | 75 ++---- .../relatorios/cartoes/cards-overview.tsx | 239 ++++++++---------- .../establishments-list.tsx | 2 +- .../top-estabelecimentos/highlights-cards.tsx | 16 +- .../top-estabelecimentos/summary-cards.tsx | 6 +- 15 files changed, 453 insertions(+), 447 deletions(-) create mode 100644 components/ajustes/companion-tab.tsx diff --git a/app/(dashboard)/ajustes/page.tsx b/app/(dashboard)/ajustes/page.tsx index 010fd63..890f0b8 100644 --- a/app/(dashboard)/ajustes/page.tsx +++ b/app/(dashboard)/ajustes/page.tsx @@ -1,7 +1,7 @@ import { headers } from "next/headers"; import { redirect } from "next/navigation"; -import { ApiTokensForm } from "@/components/ajustes/api-tokens-form"; +import { CompanionTab } from "@/components/ajustes/companion-tab"; import { DeleteAccountForm } from "@/components/ajustes/delete-account-form"; import { PreferencesForm } from "@/components/ajustes/preferences-form"; import { UpdateEmailForm } from "@/components/ajustes/update-email-form"; @@ -33,7 +33,7 @@ export default async function Page() { Preferências - Dispositivos + Companion Alterar nome Alterar senha Alterar e-mail @@ -61,20 +61,8 @@ export default async function Page() { - - -
-
-

OpenSheets Companion

-

- Conecte o app Android OpenSheets Companion para capturar - automaticamente notificações de transações financeiras e - enviá-las para sua caixa de entrada. -

-
- -
-
+ + diff --git a/app/(dashboard)/relatorios/cartoes/loading.tsx b/app/(dashboard)/relatorios/cartoes/loading.tsx index c337eb5..8d91195 100644 --- a/app/(dashboard)/relatorios/cartoes/loading.tsx +++ b/app/(dashboard)/relatorios/cartoes/loading.tsx @@ -3,83 +3,86 @@ import { Skeleton } from "@/components/ui/skeleton"; export default function Loading() { return ( -
-
- - +
+ {/* MonthNavigation skeleton */} + + + {/* Summary stats */} +
+ {[1, 2, 3, 4].map((i) => ( + + + + + + + ))}
- + {/* Cards grid */} +
+ {[1, 2, 3, 4].map((i) => ( + + ))} +
-
-
- - - - - -
- - - -
-
- {[1, 2, 3].map((i) => ( - - ))} -
-
-
-
- -
- - - - - - - - - - - -
- - - - - - {[1, 2, 3, 4, 5].map((i) => ( - - ))} - - - - - - - - - {[1, 2, 3, 4, 5].map((i) => ( - - ))} - - + {/* CardUsageChart */} + + +
+ +
+ + +
+
+ + + +
- - - - - - {[1, 2, 3, 4, 5, 6].map((i) => ( - - ))} - - -
+ {/* CategoryBreakdown + TopExpenses */} +
+ + + + + + {[1, 2, 3, 4].map((i) => ( + + ))} + + + + + + + + + {[1, 2, 3, 4].map((i) => ( + + ))} + +
+ + {/* CardInvoiceStatus - timeline minimalista */} + + + + + +
+ {[1, 2, 3, 4, 5, 6].map((i) => ( +
+ + +
+ ))} +
+
+
); } diff --git a/app/(dashboard)/relatorios/cartoes/page.tsx b/app/(dashboard)/relatorios/cartoes/page.tsx index 2027637..e35e36c 100644 --- a/app/(dashboard)/relatorios/cartoes/page.tsx +++ b/app/(dashboard)/relatorios/cartoes/page.tsx @@ -5,6 +5,7 @@ import { CardInvoiceStatus } from "@/components/relatorios/cartoes/card-invoice- import { CardTopExpenses } from "@/components/relatorios/cartoes/card-top-expenses"; import { CardUsageChart } from "@/components/relatorios/cartoes/card-usage-chart"; import { CardsOverview } from "@/components/relatorios/cartoes/cards-overview"; +import { Card } from "@/components/ui/card"; import { getUser } from "@/lib/auth/server"; import { fetchCartoesReportData } from "@/lib/relatorios/cartoes-report"; import { parsePeriodParam } from "@/lib/utils/period"; @@ -43,43 +44,37 @@ export default async function RelatorioCartoesPage({
-
-
- -
+ -
- {data.selectedCard ? ( - <> - + {data.selectedCard ? ( + <> + -
- - -
+ - - - ) : ( -
- -

Nenhum cartão selecionado

-

- Selecione um cartão na lista ao lado para ver detalhes. -

-
- )} -
-
+
+ + +
+ + ) : ( + +
+ +
+

Nenhum cartão selecionado

+

+ Selecione um cartão para ver os detalhes de uso. +

+
+ )}
); } diff --git a/app/(dashboard)/top-estabelecimentos/page.tsx b/app/(dashboard)/top-estabelecimentos/page.tsx index 65ff9d5..f90efbc 100644 --- a/app/(dashboard)/top-estabelecimentos/page.tsx +++ b/app/(dashboard)/top-estabelecimentos/page.tsx @@ -54,7 +54,7 @@ export default async function TopEstabelecimentosPage({
- Selecione o período + Selecione o intervalo de meses @@ -63,8 +63,8 @@ export default async function TopEstabelecimentosPage({ -
-
+
+
diff --git a/app/globals.css b/app/globals.css index 47de5d2..ea6c19b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -28,7 +28,7 @@ --muted-foreground: oklch(45% 0.015 60); /* Accent - complementary warm tone */ - --accent: oklch(93.996% 0.01787 64.782); + --accent: oklch(96.563% 0.00504 67.275); --accent-foreground: oklch(22% 0.025 45); /* Destructive - accessible red */ @@ -59,7 +59,7 @@ --sidebar-ring: oklch(69.18% 0.18855 38.353); /* Layout */ - --radius: 0.8rem; + --radius: 1rem; /* Shadows - warm tinted for cohesion */ --shadow-2xs: 0 1px 2px 0px oklch(35% 0.02 45 / 0.04); diff --git a/components/ajustes/api-tokens-form.tsx b/components/ajustes/api-tokens-form.tsx index f46d191..01213c1 100644 --- a/components/ajustes/api-tokens-form.tsx +++ b/components/ajustes/api-tokens-form.tsx @@ -27,7 +27,6 @@ import { } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, @@ -253,69 +252,57 @@ export function ApiTokensForm({ tokens }: ApiTokensFormProps) {
{activeTokens.length === 0 ? ( - - - -

- Nenhum dispositivo conectado. -

-

- Crie um token para conectar o app OpenSheets Companion. -

-
-
+
+ +

+ Nenhum dispositivo conectado. Crie um token para começar. +

+
) : ( -
+
{activeTokens.map((token) => ( - - -
-
-
- -
-
-
- {token.name} - - {token.tokenPrefix}... - -
-
- {token.lastUsedAt ? ( - - Usado{" "} - {formatDistanceToNow(token.lastUsedAt, { - addSuffix: true, - locale: ptBR, - })} - {token.lastUsedIp && ( - - ({token.lastUsedIp}) - - )} - - ) : ( - Nunca usado - )} -
-
- Criado em{" "} - {new Date(token.createdAt).toLocaleDateString("pt-BR")} -
-
-
- +
+
+
+
- - +
+
+ {token.name} + + {token.tokenPrefix}... + +
+

+ {token.lastUsedAt ? ( + <> + Usado{" "} + {formatDistanceToNow(token.lastUsedAt, { + addSuffix: true, + locale: ptBR, + })} + + ) : ( + "Nunca usado" + )} + {" · "} + Criado em{" "} + {new Date(token.createdAt).toLocaleDateString("pt-BR")} +

+
+
+ +
))}
)} diff --git a/components/ajustes/companion-tab.tsx b/components/ajustes/companion-tab.tsx new file mode 100644 index 0000000..f5ed8a1 --- /dev/null +++ b/components/ajustes/companion-tab.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { + RiAndroidLine, + RiDownload2Line, + RiNotification3Line, + RiQrCodeLine, + RiShieldCheckLine, +} from "@remixicon/react"; +import { Card } from "@/components/ui/card"; +import { ApiTokensForm } from "./api-tokens-form"; + +interface ApiToken { + id: string; + name: string; + tokenPrefix: string; + lastUsedAt: Date | null; + lastUsedIp: string | null; + createdAt: Date; + expiresAt: Date | null; + revokedAt: Date | null; +} + +interface CompanionTabProps { + tokens: ApiToken[]; +} + +const steps = [ + { + icon: RiDownload2Line, + title: "Instale o app", + description: "Instale o APK.", + }, + { + icon: RiQrCodeLine, + title: "Gere um token", + description: "Crie um token abaixo para autenticar.", + }, + { + icon: RiNotification3Line, + title: "Configure permissões", + description: "Conceda acesso às notificações.", + }, + { + icon: RiShieldCheckLine, + title: "Pronto!", + description: "Notificações serão enviadas ao OpenSheets.", + }, +]; + +export function CompanionTab({ tokens }: CompanionTabProps) { + return ( + +
+ {/* Header */} +
+
+

OpenSheets Companion

+ + + Android + +
+

+ Capture notificações de transações dos seus apps de banco (Nubank, + Itaú, Bradesco, Inter, C6 e outros) e envie para sua caixa de + entrada. +

+
+ + {/* Steps */} +
+ {steps.map((step, index) => ( +
+
+ +
+
+

+ {index + 1}. {step.title} +

+

+ {step.description} +

+
+
+ ))} +
+ + {/* Divider */} +
+ + {/* Devices */} + +
+ + ); +} diff --git a/components/pre-lancamentos/inbox-page.tsx b/components/pre-lancamentos/inbox-page.tsx index 017803b..0b8704a 100644 --- a/components/pre-lancamentos/inbox-page.tsx +++ b/components/pre-lancamentos/inbox-page.tsx @@ -147,7 +147,7 @@ export function InboxPage({ } title="Nenhum pré-lançamento" - description="As notificações capturadas pelo app OpenSheets Companion aparecerão aqui para você processar." + description="As notificações capturadas pelo app OpenSheets Companion aparecerão aqui para você processar. Saiba mais sobre o app em Ajustes > Companion." /> ) : ( diff --git a/components/relatorios/cartoes/card-invoice-status.tsx b/components/relatorios/cartoes/card-invoice-status.tsx index 751aeaf..ff31357 100644 --- a/components/relatorios/cartoes/card-invoice-status.tsx +++ b/components/relatorios/cartoes/card-invoice-status.tsx @@ -1,9 +1,15 @@ "use client"; -import { RiBankCard2Fill } from "@remixicon/react"; -import { Badge } from "@/components/ui/badge"; +import { RiCalendarCheckLine } from "@remixicon/react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import type { CardDetailData } from "@/lib/relatorios/cartoes-report"; +import { cn } from "@/lib/utils"; import { title_font } from "@/public/fonts/font_index"; type CardInvoiceStatusProps = { @@ -35,47 +41,35 @@ export function CardInvoiceStatus({ data }: CardInvoiceStatusProps) { }).format(value); }; - const getStatusBadge = (status: string | null) => { + const getStatusColor = (status: string | null) => { switch (status) { case "pago": - return ( - - Pago - - ); + return "bg-green-500"; case "pendente": - return ( - - Pendente - - ); + return "bg-yellow-500"; case "atrasado": - return ( - - Atrasado - - ); + return "bg-red-500"; default: - return ( - - — - - ); + return "bg-muted"; } }; - const formatPeriod = (period: string) => { - const [year, month] = period.split("-"); - return `${monthLabels[parseInt(month, 10) - 1]}/${year.slice(2)}`; + const getStatusLabel = (status: string | null) => { + switch (status) { + case "pago": + return "Pago"; + case "pendente": + return "Pendente"; + case "atrasado": + return "Atrasado"; + default: + return "—"; + } + }; + + const formatPeriodShort = (period: string) => { + const [, month] = period.split("-"); + return monthLabels[parseInt(month, 10) - 1]; }; return ( @@ -84,29 +78,38 @@ export function CardInvoiceStatus({ data }: CardInvoiceStatusProps) { - - Status das Faturas + + Faturas -
- {[...data].reverse().map((invoice) => ( -
-
- - {formatPeriod(invoice.period)} - - {getStatusBadge(invoice.status)} -
- - {formatCurrency(invoice.amount)} - -
- ))} -
+ +
+ {data.map((invoice) => ( + + +
+
+ + {formatPeriodShort(invoice.period)} + +
+ + +

+ {formatCurrency(invoice.amount)} +

+

{getStatusLabel(invoice.status)}

+
+ + ))} +
+ ); diff --git a/components/relatorios/cartoes/card-top-expenses.tsx b/components/relatorios/cartoes/card-top-expenses.tsx index 4358fbf..8194c52 100644 --- a/components/relatorios/cartoes/card-top-expenses.tsx +++ b/components/relatorios/cartoes/card-top-expenses.tsx @@ -90,7 +90,7 @@ export function CardTopExpenses({ data }: CardTopExpensesProps) { {/* Value */}
diff --git a/components/relatorios/cartoes/card-usage-chart.tsx b/components/relatorios/cartoes/card-usage-chart.tsx index d2e59f9..a162fc3 100644 --- a/components/relatorios/cartoes/card-usage-chart.tsx +++ b/components/relatorios/cartoes/card-usage-chart.tsx @@ -1,8 +1,7 @@ "use client"; -import { RiBankCard2Line } from "@remixicon/react"; +import { RiBankCard2Line, RiBarChartBoxLine } from "@remixicon/react"; import Image from "next/image"; -import { useState } from "react"; import { Bar, BarChart, @@ -11,15 +10,14 @@ import { XAxis, YAxis, } from "recharts"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { type ChartConfig, ChartContainer, ChartTooltip, } from "@/components/ui/chart"; import type { CardDetailData } from "@/lib/relatorios/cartoes-report"; -import { cn } from "@/lib/utils"; +import { title_font } from "@/public/fonts/font_index"; type CardUsageChartProps = { data: CardDetailData["monthlyUsage"]; @@ -37,14 +35,6 @@ const chartConfig = { }, } satisfies ChartConfig; -type PeriodFilter = "3" | "6" | "12"; - -const filterOptions: { value: PeriodFilter; label: string }[] = [ - { value: "3", label: "3 meses" }, - { value: "6", label: "6 meses" }, - { value: "12", label: "12 meses" }, -]; - const resolveLogoPath = (logo: string | null) => { if (!logo) return null; if ( @@ -58,8 +48,6 @@ const resolveLogoPath = (logo: string | null) => { }; export function CardUsageChart({ data, limit, card }: CardUsageChartProps) { - const [period, setPeriod] = useState("6"); - const formatCurrency = (value: number) => { return new Intl.NumberFormat("pt-BR", { style: "currency", @@ -82,10 +70,8 @@ export function CardUsageChart({ data, limit, card }: CardUsageChartProps) { return formatCurrency(value); }; - // Filter data based on selected period - const filteredData = data.slice(-Number(period)); - - const chartData = filteredData.map((item) => ({ + // Always show last 12 months + const chartData = data.slice(-12).map((item) => ({ month: item.periodLabel, amount: item.amount, })); @@ -96,42 +82,29 @@ export function CardUsageChart({ data, limit, card }: CardUsageChartProps) {
- {/* Card logo and name on the left */} + + + Histórico de Uso + + + {/* Card logo and name */}
{logoPath ? ( -
- {`Logo -
+ {card.name} ) : ( -
- -
+ )} - {card.name} -
- - {/* Filters on the right */} -
- {filterOptions.map((option) => ( - - ))} + + {card.name} +
diff --git a/components/relatorios/cartoes/cards-overview.tsx b/components/relatorios/cartoes/cards-overview.tsx index 03e6e9a..5f0f156 100644 --- a/components/relatorios/cartoes/cards-overview.tsx +++ b/components/relatorios/cartoes/cards-overview.tsx @@ -1,18 +1,14 @@ "use client"; -import { - RiArrowDownLine, - RiArrowUpLine, - RiBankCard2Line, -} from "@remixicon/react"; +import { RiBankCard2Line } from "@remixicon/react"; import Image from "next/image"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import MoneyValues from "@/components/money-values"; +import { Card, CardContent } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import type { CartoesReportData } from "@/lib/relatorios/cartoes-report"; import { cn } from "@/lib/utils"; -import { title_font } from "@/public/fonts/font_index"; type CardsOverviewProps = { data: CartoesReportData; @@ -53,14 +49,13 @@ export function CardsOverview({ data }: CardsOverviewProps) { const searchParams = useSearchParams(); const periodoParam = searchParams.get("periodo"); - const formatCurrency = (value: number) => { - return new Intl.NumberFormat("pt-BR", { + const formatCurrency = (value: number) => + new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL", minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(value); - }; const getUsageColor = (percent: number) => { if (percent < 50) return "bg-green-500"; @@ -75,155 +70,119 @@ export function CardsOverview({ data }: CardsOverviewProps) { return `/relatorios/cartoes?${params.toString()}`; }; + const summaryCards = [ + { title: "Limite", value: data.totalLimit, isMoney: true }, + { title: "Usado", value: data.totalUsage, isMoney: true }, + { + title: "Disponível", + value: data.totalLimit - data.totalUsage, + isMoney: true, + }, + { title: "Utilização", value: data.totalUsagePercent, isMoney: false }, + ]; + if (data.cards.length === 0) { return ( - - - - Resumo dos Cartões - - - -
- -

Nenhum cartão ativo encontrado

-
+ + +

Nenhum cartão encontrado

); } return ( - - - - - Resumo dos Cartões - - - -
-
-

Limite Total

-

- {formatCurrency(data.totalLimit)} -

-
-
-

Uso Total

-

- {formatCurrency(data.totalUsage)} -

-
-
-

Utilização

-

- {data.totalUsagePercent.toFixed(0)}% -

-
-
+
+ {/* Summary stats */} +
+ {summaryCards.map((card) => ( + + +

{card.title}

+ {card.isMoney ? ( + + ) : ( +

+ {card.value.toFixed(0)}% +

+ )} +
+
+ ))} +
-
- {data.cards.map((card) => { - const logoPath = resolveLogoPath(card.logo); - const brandAsset = resolveBrandAsset(card.brand); +

Meus cartões

- return ( + {/* Cards list */} +
+ {data.cards.map((card) => { + const logoPath = resolveLogoPath(card.logo); + const brandAsset = resolveBrandAsset(card.brand); + const isSelected = data.selectedCard?.card.id === card.id; + + return ( + -
-
- {/* Logo container - size-10 like expenses-by-category */} -
- {logoPath ? ( - {`Logo - ) : ( - - )} -
- - {/* Name and brand */} -
-
- - {card.name} - - {brandAsset && ( - {`Bandeira - )} -
-
- - {formatCurrency(card.currentUsage)} /{" "} - {formatCurrency(card.limit)} - -
-
+
+ {logoPath ? ( + {card.name} + ) : ( + + )} +
+
+
+ + {card.name} + + {brandAsset && ( + {card.brand + )}
- - {/* Trend and percentage */} -
- +

+ {formatCurrency(card.currentUsage)} /{" "} + {formatCurrency(card.limit)} +

+
+ div]:${getUsageColor(card.usagePercent)}`, + )} + /> + {card.usagePercent.toFixed(0)}% -
- {card.trend === "up" && ( - - )} - {card.trend === "down" && ( - - )} - - {card.changePercent > 0 ? "+" : ""} - {card.changePercent.toFixed(0)}% - -
- - {/* Progress bar - aligned with content */} -
- div]:${getUsageColor(card.usagePercent)}`, - )} - /> -
- ); - })} -
- - + + ); + })} +
+
); } diff --git a/components/top-estabelecimentos/establishments-list.tsx b/components/top-estabelecimentos/establishments-list.tsx index 3a19d82..69f5c7c 100644 --- a/components/top-estabelecimentos/establishments-list.tsx +++ b/components/top-estabelecimentos/establishments-list.tsx @@ -105,7 +105,7 @@ export function EstablishmentsList({ {/* Value and stats */}
diff --git a/components/top-estabelecimentos/highlights-cards.tsx b/components/top-estabelecimentos/highlights-cards.tsx index 3853a6e..0378663 100644 --- a/components/top-estabelecimentos/highlights-cards.tsx +++ b/components/top-estabelecimentos/highlights-cards.tsx @@ -11,17 +11,17 @@ type HighlightsCardsProps = { export function HighlightsCards({ summary }: HighlightsCardsProps) { return (
- +
-
- +
+
-

+

Mais Frequente

-

+

{summary.mostFrequent || "—"}

@@ -29,17 +29,17 @@ export function HighlightsCards({ summary }: HighlightsCardsProps) { - +
-
+

Maior Gasto Total

-

+

{summary.highestSpending || "—"}

diff --git a/components/top-estabelecimentos/summary-cards.tsx b/components/top-estabelecimentos/summary-cards.tsx index 3685346..3b26655 100644 --- a/components/top-estabelecimentos/summary-cards.tsx +++ b/components/top-estabelecimentos/summary-cards.tsx @@ -50,7 +50,7 @@ export function SummaryCards({ summary }: SummaryCardsProps) {
{cards.map((card) => ( - +

@@ -58,11 +58,11 @@ export function SummaryCards({ summary }: SummaryCardsProps) {

{card.isMoney ? ( ) : ( -

{card.value}

+

{card.value}

)}

{card.description}