diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index 1ba8bfd..269df93 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -2,10 +2,13 @@ import { connection } from "next/server"; import { DashboardGridEditable } from "@/features/dashboard/components/dashboard-grid-editable"; import { DashboardMetricsCards } from "@/features/dashboard/components/dashboard-metrics-cards"; import { DashboardWelcome } from "@/features/dashboard/components/dashboard-welcome"; +import { extractDashboardLogoNames } from "@/features/dashboard/extract-logo-names"; import { fetchDashboardPageData } from "@/features/dashboard/page-data-queries"; import { getSingleParam } from "@/features/transactions/page-helpers"; +import { LogoPrefetchProvider } from "@/shared/components/entity-avatar"; import MonthNavigation from "@/shared/components/month-picker/month-navigation"; import { getUser } from "@/shared/lib/auth/server"; +import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server"; import { parsePeriodParam } from "@/shared/utils/period"; type PageSearchParams = Promise>; @@ -25,17 +28,24 @@ export default async function Page({ searchParams }: PageProps) { await fetchDashboardPageData(user.id, selectedPeriod); const { dashboardWidgets } = preferences; + const logoMappings = await prefetchLogoMappings( + user.id, + extractDashboardLogoNames(dashboardData), + ); + return (
- + + +
); } diff --git a/src/app/(dashboard)/payers/[payerId]/page.tsx b/src/app/(dashboard)/payers/[payerId]/page.tsx index 31d86e1..5ad79e5 100644 --- a/src/app/(dashboard)/payers/[payerId]/page.tsx +++ b/src/app/(dashboard)/payers/[payerId]/page.tsx @@ -41,6 +41,7 @@ import { fetchRecentEstablishments, fetchTransactionFilterSources, } from "@/features/transactions/queries"; +import { LogoPrefetchProvider } from "@/shared/components/entity-avatar"; import { ExpandableWidgetCard } from "@/shared/components/expandable-widget-card"; import MonthNavigation from "@/shared/components/month-picker/month-navigation"; import { @@ -50,6 +51,7 @@ import { TabsTrigger, } from "@/shared/components/ui/tabs"; import { getUserId } from "@/shared/lib/auth/server"; +import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server"; import { getPayerAccess } from "@/shared/lib/payers/access"; import { fetchPagadorBoletoItems, @@ -82,6 +84,7 @@ const EMPTY_FILTERS: TransactionSearchFilters = { searchFilter: null, settledFilter: null, attachmentFilter: null, + dividedFilter: null, }; const createEmptySlugMaps = (): SlugMaps => ({ @@ -307,104 +310,113 @@ export default async function Page({ params, searchParams }: PageProps) { lancamentoCount: transactionData.length, }; + const logoMappings = await prefetchLogoMappings(dataOwnerId, [ + ...transactionData.map((t) => t.name), + ...boletoItems.map((b) => b.name), + ]); + return (
- - - Perfil - Painel - Lançamentos - - + + + + Perfil + Painel + Lançamentos + + - - - {canEdit && payerData.shareCode ? ( - - ) : null} - {!canEdit && currentUserShare ? ( - - ) : null} - + + + {canEdit && payerData.shareCode ? ( + + ) : null} + {!canEdit && currentUserShare ? ( + + ) : null} + - -
- - -
+ +
+ + +
-
- } - > - - - } - > - - - } - > - - -
-
+
+ } + > + + + } + > + + + } + > + + +
+
- -
- -
-
-
+ +
+ +
+
+
+
); } diff --git a/src/app/(dashboard)/transactions/page.tsx b/src/app/(dashboard)/transactions/page.tsx index bd89ef6..0f143b5 100644 --- a/src/app/(dashboard)/transactions/page.tsx +++ b/src/app/(dashboard)/transactions/page.tsx @@ -17,8 +17,10 @@ import { fetchTransactionFilterSources, fetchTransactionsPage, } from "@/features/transactions/queries"; +import { LogoPrefetchProvider } from "@/shared/components/entity-avatar"; import MonthNavigation from "@/shared/components/month-picker/month-navigation"; import { getUserId } from "@/shared/lib/auth/server"; +import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server"; import { parsePeriodParam } from "@/shared/utils/period"; type PageSearchParams = Promise; @@ -74,38 +76,45 @@ export default async function Page({ searchParams }: PageProps) { payerRows: filterSources.payerRows, }); + const logoMappings = await prefetchLogoMappings( + userId, + transactionData.map((t) => t.name), + ); + return (
- + + +
); } diff --git a/src/features/dashboard/extract-logo-names.ts b/src/features/dashboard/extract-logo-names.ts new file mode 100644 index 0000000..8b64377 --- /dev/null +++ b/src/features/dashboard/extract-logo-names.ts @@ -0,0 +1,28 @@ +import type { DashboardData } from "./fetch-dashboard-data"; + +/** + * Coleta todos os nomes de estabelecimentos exibidos nos widgets do + * dashboard que renderizam ``. Usado para + * pré-resolver os mapeamentos Logo.dev no servidor. + */ +export function extractDashboardLogoNames(data: DashboardData): string[] { + const names: string[] = []; + + for (const bill of data.billsSnapshot.bills) names.push(bill.name); + for (const expense of data.recurringExpensesData.expenses) + names.push(expense.name); + for (const expense of data.installmentExpensesData.expenses) + names.push(expense.name); + for (const establishment of data.topEstablishmentsData.establishments) + names.push(establishment.name); + for (const expense of data.topExpensesAll.expenses) names.push(expense.name); + for (const expense of data.topExpensesCardOnly.expenses) + names.push(expense.name); + for (const transactions of Object.values( + data.purchasesByCategoryData.transactionsByCategory, + )) { + for (const transaction of transactions) names.push(transaction.name); + } + + return names; +} diff --git a/src/shared/components/entity-avatar/index.ts b/src/shared/components/entity-avatar/index.ts index d320004..ecdfdf1 100644 --- a/src/shared/components/entity-avatar/index.ts +++ b/src/shared/components/entity-avatar/index.ts @@ -5,3 +5,4 @@ export type { export { CategoryIconBadge } from "./category-icon-badge"; export { EstablishmentLogo } from "./establishment-logo"; export { EstablishmentLogoPicker } from "./establishment-logo-picker"; +export { LogoPrefetchProvider } from "./logo-prefetch-provider"; diff --git a/src/shared/components/entity-avatar/logo-prefetch-provider.tsx b/src/shared/components/entity-avatar/logo-prefetch-provider.tsx new file mode 100644 index 0000000..ad15c33 --- /dev/null +++ b/src/shared/components/entity-avatar/logo-prefetch-provider.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useQueryClient } from "@tanstack/react-query"; +import { type ReactNode, useRef } from "react"; +import { logoQueryKeys } from "@/shared/lib/logo"; +import type { LogoPrefetchEntry } from "@/shared/lib/logo/types"; + +type LogoPrefetchProviderProps = { + mappings: LogoPrefetchEntry[]; + children: ReactNode; +}; + +/** + * Semeia o cache do React Query com mapeamentos de logo já resolvidos + * no servidor. Evita que cada `EstablishmentLogo` dispare seu próprio + * GET para `/api/logo/mapping` no primeiro render. + */ +export function LogoPrefetchProvider({ + mappings, + children, +}: LogoPrefetchProviderProps) { + const queryClient = useQueryClient(); + const seeded = useRef(false); + + if (!seeded.current) { + for (const { nameKey, domain, logoUrl } of mappings) { + queryClient.setQueryData(logoQueryKeys.mapping(nameKey), { + domain, + logoUrl, + }); + } + seeded.current = true; + } + + return <>{children}; +} diff --git a/src/shared/lib/logo/prefetch-server.ts b/src/shared/lib/logo/prefetch-server.ts new file mode 100644 index 0000000..eae77f7 --- /dev/null +++ b/src/shared/lib/logo/prefetch-server.ts @@ -0,0 +1,31 @@ +import "server-only"; + +import { fetchEstablishmentLogoMap } from "./establishment-logo-queries"; +import { toNameKey } from "./index"; +import { buildLogoDevUrl } from "./server"; +import type { LogoPrefetchEntry } from "./types"; + +export async function prefetchLogoMappings( + userId: string, + names: string[], +): Promise { + const uniqueNames = [ + ...new Set( + names.filter((n) => typeof n === "string" && n.trim().length > 0), + ), + ]; + if (uniqueNames.length === 0) return []; + + const map = await fetchEstablishmentLogoMap(userId, uniqueNames); + + const seen = new Set(); + const entries: LogoPrefetchEntry[] = []; + for (const name of uniqueNames) { + const nameKey = toNameKey(name); + if (seen.has(nameKey)) continue; + seen.add(nameKey); + const domain = map.get(nameKey) ?? null; + entries.push({ nameKey, domain, logoUrl: buildLogoDevUrl(domain) }); + } + return entries; +} diff --git a/src/shared/lib/logo/types.ts b/src/shared/lib/logo/types.ts new file mode 100644 index 0000000..19d784a --- /dev/null +++ b/src/shared/lib/logo/types.ts @@ -0,0 +1,5 @@ +export type LogoPrefetchEntry = { + nameKey: string; + domain: string | null; + logoUrl: string | null; +};