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",