From 4b442a907a597f72880d95928381441503aa595f Mon Sep 17 00:00:00 2001
From: Felipe Coutinho
Date: Sun, 15 Feb 2026 00:06:54 +0000
Subject: [PATCH] =?UTF-8?q?feat(v1.4.1):=20tabs=20de=20hist=C3=B3rico,=20l?=
=?UTF-8?q?ogo=20matching=20e=20melhorias=20nos=20pr=C3=A9-lan=C3=A7amento?=
=?UTF-8?q?s?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Opus 4.6
---
CHANGELOG.md | 21 ++
app/(dashboard)/pre-lancamentos/data.ts | 25 +++
app/(dashboard)/pre-lancamentos/layout.tsx | 2 +-
app/(dashboard)/pre-lancamentos/page.tsx | 19 +-
app/globals.css | 4 +-
.../shared/estabelecimento-logo.tsx | 4 +-
components/pre-lancamentos/inbox-card.tsx | 186 +++++++++++++-----
components/pre-lancamentos/inbox-page.tsx | 138 +++++++++----
lib/actions/helpers.ts | 2 +-
package.json | 2 +-
10 files changed, 305 insertions(+), 98 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5b7aff..d63bdea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,27 @@ 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.4.1] - 2026-02-15
+
+### Adicionado
+
+- Abas "Pendentes", "Processados" e "Descartados" na página de pré-lançamentos (antes exibia apenas pendentes)
+- Logo do cartão/conta exibido automaticamente nos cards de pré-lançamento via matching por nome do app
+- Pre-fill automático do cartão de crédito ao processar pré-lançamento (match pelo nome do app)
+- Badge de status e data nos cards de itens já processados/descartados (modo readonly)
+
+### Corrigido
+
+- `revalidateTag("dashboard", "max")` para invalidar todas as entradas de cache da tag (antes invalidava apenas a mais recente)
+- Cor `--warning` ajustada para melhor contraste (mais alaranjada)
+- `EstabelecimentoLogo` não precisava de `"use client"` — removido
+- Fallback no cálculo de `fontSize` em `EstabelecimentoLogo`
+
+### Alterado
+
+- Nome do estabelecimento formatado em Title Case ao processar pré-lançamento
+- Subtítulo da página de pré-lançamentos atualizado
+
## [1.4.0] - 2026-02-07
### Corrigido
diff --git a/app/(dashboard)/pre-lancamentos/data.ts b/app/(dashboard)/pre-lancamentos/data.ts
index f32b32f..1cbb206 100644
--- a/app/(dashboard)/pre-lancamentos/data.ts
+++ b/app/(dashboard)/pre-lancamentos/data.ts
@@ -88,6 +88,31 @@ export async function fetchCartoesForSelect(
return items;
}
+export async function fetchAppLogoMap(
+ userId: string,
+): Promise> {
+ const [userCartoes, userContas] = await Promise.all([
+ db
+ .select({ name: cartoes.name, logo: cartoes.logo })
+ .from(cartoes)
+ .where(eq(cartoes.userId, userId)),
+ db
+ .select({ name: contas.name, logo: contas.logo })
+ .from(contas)
+ .where(eq(contas.userId, userId)),
+ ]);
+
+ const logoMap: Record = {};
+
+ for (const item of [...userCartoes, ...userContas]) {
+ if (item.logo) {
+ logoMap[item.name.toLowerCase()] = item.logo;
+ }
+ }
+
+ return logoMap;
+}
+
export async function fetchPendingInboxCount(userId: string): Promise {
const items = await db
.select({ id: preLancamentos.id })
diff --git a/app/(dashboard)/pre-lancamentos/layout.tsx b/app/(dashboard)/pre-lancamentos/layout.tsx
index d771a79..fe31e3b 100644
--- a/app/(dashboard)/pre-lancamentos/layout.tsx
+++ b/app/(dashboard)/pre-lancamentos/layout.tsx
@@ -15,7 +15,7 @@ export default function RootLayout({
}
title="Pré-Lançamentos"
- subtitle="Notificações capturadas aguardando processamento"
+ subtitle="Notificações capturadas pelo Companion"
/>
{children}
diff --git a/app/(dashboard)/pre-lancamentos/page.tsx b/app/(dashboard)/pre-lancamentos/page.tsx
index 6a11212..14526a0 100644
--- a/app/(dashboard)/pre-lancamentos/page.tsx
+++ b/app/(dashboard)/pre-lancamentos/page.tsx
@@ -1,19 +1,25 @@
import { InboxPage } from "@/components/pre-lancamentos/inbox-page";
import { getUserId } from "@/lib/auth/server";
-import { fetchInboxDialogData, fetchInboxItems } from "./data";
+import { fetchAppLogoMap, fetchInboxDialogData, fetchInboxItems } from "./data";
export default async function Page() {
const userId = await getUserId();
- const [items, dialogData] = await Promise.all([
- fetchInboxItems(userId, "pending"),
- fetchInboxDialogData(userId),
- ]);
+ const [pendingItems, processedItems, discardedItems, dialogData, appLogoMap] =
+ await Promise.all([
+ fetchInboxItems(userId, "pending"),
+ fetchInboxItems(userId, "processed"),
+ fetchInboxItems(userId, "discarded"),
+ fetchInboxDialogData(userId),
+ fetchAppLogoMap(userId),
+ ]);
return (
);
diff --git a/app/globals.css b/app/globals.css
index 2595612..e5d75c1 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -34,7 +34,7 @@
/* Semantic states */
--success: oklch(55% 0.17 150);
--success-foreground: oklch(98% 0.01 150);
- --warning: oklch(75.976% 0.16034 71.493);
+ --warning: oklch(69.913% 0.1798 49.649);
--warning-foreground: oklch(20% 0.04 85);
--info: oklch(55% 0.17 250);
--info-foreground: oklch(98% 0.01 250);
@@ -123,7 +123,7 @@
/* Semantic states */
--success: oklch(65% 0.19 150);
--success-foreground: oklch(15% 0.02 150);
- --warning: oklch(75.976% 0.16034 71.493);
+ --warning: oklch(69.913% 0.1798 49.649);
--warning-foreground: oklch(15% 0.04 85);
--info: oklch(65% 0.17 250);
--info-foreground: oklch(15% 0.02 250);
diff --git a/components/lancamentos/shared/estabelecimento-logo.tsx b/components/lancamentos/shared/estabelecimento-logo.tsx
index 2e60654..fc9838a 100644
--- a/components/lancamentos/shared/estabelecimento-logo.tsx
+++ b/components/lancamentos/shared/estabelecimento-logo.tsx
@@ -1,5 +1,3 @@
-"use client";
-
import { cn } from "@/lib/utils/ui";
interface EstabelecimentoLogoProps {
@@ -63,7 +61,7 @@ export function EstabelecimentoLogo({
style={{
width: size,
height: size,
- fontSize: size * 0.4,
+ fontSize: (size ?? 32) * 0.4,
}}
>
{initials}
diff --git a/components/pre-lancamentos/inbox-card.tsx b/components/pre-lancamentos/inbox-card.tsx
index 0c3d090..ce48df1 100644
--- a/components/pre-lancamentos/inbox-card.tsx
+++ b/components/pre-lancamentos/inbox-card.tsx
@@ -6,9 +6,12 @@ import {
RiEyeLine,
RiMoreLine,
} from "@remixicon/react";
-import { formatDistanceToNow } from "date-fns";
+import { format, formatDistanceToNow } from "date-fns";
import { ptBR } from "date-fns/locale";
+import Image from "next/image";
+import { useMemo } from "react";
import MoneyValues from "@/components/money-values";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -28,17 +31,59 @@ import type { InboxItem } from "./types";
interface InboxCardProps {
item: InboxItem;
- onProcess: (item: InboxItem) => void;
- onDiscard: (item: InboxItem) => void;
- onViewDetails: (item: InboxItem) => void;
+ readonly?: boolean;
+ appLogoMap?: Record;
+ onProcess?: (item: InboxItem) => void;
+ onDiscard?: (item: InboxItem) => void;
+ onViewDetails?: (item: InboxItem) => void;
+}
+
+function resolveLogoPath(logo: string): string {
+ if (
+ logo.startsWith("http") ||
+ logo.startsWith("data:") ||
+ logo.startsWith("/")
+ ) {
+ return logo;
+ }
+ return `/logos/${logo}`;
+}
+
+function findMatchingLogo(
+ sourceAppName: string | null,
+ appLogoMap: Record,
+): string | null {
+ if (!sourceAppName) return null;
+
+ const appName = sourceAppName.toLowerCase();
+
+ // Exact match first
+ if (appLogoMap[appName]) return resolveLogoPath(appLogoMap[appName]);
+
+ // Partial match: card/account name contains app name or vice versa
+ for (const [name, logo] of Object.entries(appLogoMap)) {
+ if (name.includes(appName) || appName.includes(name)) {
+ return resolveLogoPath(logo);
+ }
+ }
+
+ return null;
}
export function InboxCard({
item,
+ readonly,
+ appLogoMap,
onProcess,
onDiscard,
onViewDetails,
}: InboxCardProps) {
+ const matchedLogo = useMemo(
+ () =>
+ appLogoMap ? findMatchingLogo(item.sourceAppName, appLogoMap) : null,
+ [item.sourceAppName, appLogoMap],
+ );
+
const amount = item.parsedAmount ? parseFloat(item.parsedAmount) : null;
// O timestamp vem do app Android em horário local mas salvo como UTC
@@ -63,12 +108,32 @@ export function InboxCard({
timeZone: "UTC",
}).format(rawDate);
+ const statusDate =
+ item.status === "processed"
+ ? item.processedAt
+ : item.status === "discarded"
+ ? item.discardedAt
+ : null;
+
+ const formattedStatusDate = statusDate
+ ? format(new Date(statusDate), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })
+ : null;
+
return (
{/* Header com app e valor */}
-
+
+ {matchedLogo && (
+
+ )}
{item.sourceAppName || item.sourceApp}
{" "}
@@ -80,36 +145,38 @@ export function InboxCard({
)}
-
-
-
-
-
-
- onViewDetails(item)}>
-
- Ver detalhes
-
- onProcess(item)}>
-
- Processar
-
- onDiscard(item)}
- className="text-destructive"
- >
-
- Descartar
-
-
-
-
+ {!readonly && (
+
+
+
+
+
+
+ onViewDetails?.(item)}>
+
+ Ver detalhes
+
+ onProcess?.(item)}>
+
+ Processar
+
+ onDiscard?.(item)}
+ className="text-destructive"
+ >
+
+ Descartar
+
+
+
+
+ )}
{/* Conteúdo da notificação */}
@@ -122,21 +189,40 @@ export function InboxCard({
- {/* Botões de ação */}
-
-
-
-
+ {/* Botões de ação ou badge de status */}
+ {readonly ? (
+
+
+ {item.status === "processed" ? "Processado" : "Descartado"}
+
+ {formattedStatusDate && (
+
+ {formattedStatusDate}
+
+ )}
+
+ ) : (
+
+
+
+
+ )}
);
}
diff --git a/components/pre-lancamentos/inbox-page.tsx b/components/pre-lancamentos/inbox-page.tsx
index 0b8704a..b5afe3a 100644
--- a/components/pre-lancamentos/inbox-page.tsx
+++ b/components/pre-lancamentos/inbox-page.tsx
@@ -11,12 +11,15 @@ import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
import { EmptyState } from "@/components/empty-state";
import { LancamentoDialog } from "@/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog";
import { Card } from "@/components/ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { InboxCard } from "./inbox-card";
import { InboxDetailsDialog } from "./inbox-details-dialog";
import type { InboxItem, SelectOption } from "./types";
interface InboxPageProps {
- items: InboxItem[];
+ pendingItems: InboxItem[];
+ processedItems: InboxItem[];
+ discardedItems: InboxItem[];
pagadorOptions: SelectOption[];
splitPagadorOptions: SelectOption[];
defaultPagadorId: string | null;
@@ -24,10 +27,13 @@ interface InboxPageProps {
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
estabelecimentos: string[];
+ appLogoMap: Record;
}
export function InboxPage({
- items,
+ pendingItems,
+ processedItems,
+ discardedItems,
pagadorOptions,
splitPagadorOptions,
defaultPagadorId,
@@ -35,6 +41,7 @@ export function InboxPage({
cartaoOptions,
categoriaOptions,
estabelecimentos,
+ appLogoMap,
}: InboxPageProps) {
const [processOpen, setProcessOpen] = useState(false);
const [itemToProcess, setItemToProcess] = useState(null);
@@ -45,14 +52,24 @@ export function InboxPage({
const [discardOpen, setDiscardOpen] = useState(false);
const [itemToDiscard, setItemToDiscard] = useState(null);
- const sortedItems = useMemo(
- () =>
- [...items].sort(
- (a, b) =>
- new Date(b.notificationTimestamp).getTime() -
- new Date(a.notificationTimestamp).getTime(),
- ),
- [items],
+ const sortByTimestamp = (list: InboxItem[]) =>
+ [...list].sort(
+ (a, b) =>
+ new Date(b.notificationTimestamp).getTime() -
+ new Date(a.notificationTimestamp).getTime(),
+ );
+
+ const sortedPending = useMemo(
+ () => sortByTimestamp(pendingItems),
+ [pendingItems],
+ );
+ const sortedProcessed = useMemo(
+ () => sortByTimestamp(processedItems),
+ [processedItems],
+ );
+ const sortedDiscarded = useMemo(
+ () => sortByTimestamp(discardedItems),
+ [discardedItems],
);
const handleProcessOpenChange = useCallback((open: boolean) => {
@@ -133,37 +150,88 @@ export function InboxPage({
const defaultPurchaseDate =
getDateString(itemToProcess?.notificationTimestamp) ?? null;
- const defaultName = itemToProcess?.parsedName ?? null;
+ const defaultName = itemToProcess?.parsedName
+ ? itemToProcess.parsedName
+ .toLowerCase()
+ .replace(/\b\w/g, (char) => char.toUpperCase())
+ : null;
const defaultAmount = itemToProcess?.parsedAmount
? String(Math.abs(Number(itemToProcess.parsedAmount)))
: null;
+ // Match sourceAppName with a cartão to pre-fill card select
+ const matchedCartaoId = useMemo(() => {
+ const appName = itemToProcess?.sourceAppName?.toLowerCase();
+ if (!appName) return null;
+
+ for (const option of cartaoOptions) {
+ const label = option.label.toLowerCase();
+ if (label.includes(appName) || appName.includes(label)) {
+ return option.value;
+ }
+ }
+ return null;
+ }, [itemToProcess?.sourceAppName, cartaoOptions]);
+
+ const renderEmptyState = (message: string) => (
+
+ }
+ title={message}
+ description="As notificações capturadas pelo app OpenSheets Companion aparecerão aqui. Saiba mais em Ajustes > Companion."
+ />
+
+ );
+
+ const renderGrid = (list: InboxItem[], readonly?: boolean) =>
+ list.length === 0 ? (
+ renderEmptyState(
+ readonly
+ ? "Nenhuma notificação nesta aba"
+ : "Nenhum pré-lançamento pendente",
+ )
+ ) : (
+
+ {list.map((item) => (
+
+ ))}
+
+ );
+
return (
<>
-
- {sortedItems.length === 0 ? (
-
- }
- title="Nenhum pré-lançamento"
- description="As notificações capturadas pelo app OpenSheets Companion aparecerão aqui para você processar. Saiba mais sobre o app em Ajustes > Companion."
- />
-
- ) : (
-
- {sortedItems.map((item) => (
-
- ))}
-
- )}
-
+
+
+
+ Pendentes ({pendingItems.length})
+
+
+ Processados ({processedItems.length})
+
+
+ Descartados ({discardedItems.length})
+
+
+
+
+ {renderGrid(sortedPending)}
+
+
+ {renderGrid(sortedProcessed, true)}
+
+
+ {renderGrid(sortedDiscarded, true)}
+
+
diff --git a/lib/actions/helpers.ts b/lib/actions/helpers.ts
index a7c44b0..23d4b01 100644
--- a/lib/actions/helpers.ts
+++ b/lib/actions/helpers.ts
@@ -57,7 +57,7 @@ export function revalidateForEntity(
// Invalidate dashboard cache for financial mutations
if (DASHBOARD_ENTITIES.has(entity)) {
- revalidateTag("dashboard");
+ revalidateTag("dashboard", "max");
}
}
diff --git a/package.json b/package.json
index f18ebb8..d42c02a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "opensheets",
- "version": "1.4.0",
+ "version": "1.4.1",
"private": true,
"scripts": {
"dev": "next dev --turbopack",