perf(logos): pré-resolver mapeamentos Logo.dev no servidor

Cada EstablishmentLogo dispara um GET para /api/logo/mapping por
nome único (deduplicado pelo React Query, mas ainda N requests por
página). Em /dashboard, /transactions e /payers/[payerId] agora
fazemos uma única query SQL em batch (fetchEstablishmentLogoMap) e
semeamos o cache do React Query antes do primeiro render via novo
LogoPrefetchProvider — eliminando os requests da rede.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-04-25 14:45:54 +00:00
parent 7f05d2a681
commit b453b432ed
8 changed files with 257 additions and 125 deletions

View File

@@ -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<Record<string, string | string[] | undefined>>;
@@ -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 (
<main className="flex flex-col gap-4">
<DashboardWelcome name={user.name} />
<MonthNavigation />
<DashboardMetricsCards metrics={dashboardData.metrics} />
<DashboardGridEditable
data={dashboardData}
period={selectedPeriod}
initialPreferences={dashboardWidgets}
quickActionOptions={quickActionOptions}
/>
<LogoPrefetchProvider mappings={logoMappings}>
<DashboardGridEditable
data={dashboardData}
period={selectedPeriod}
initialPreferences={dashboardWidgets}
quickActionOptions={quickActionOptions}
/>
</LogoPrefetchProvider>
</main>
);
}

View File

@@ -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 (
<main className="flex flex-col gap-6">
<MonthNavigation />
<Tabs defaultValue="profile" className="w-full">
<TabsList className="mb-2">
<TabsTrigger value="profile">Perfil</TabsTrigger>
<TabsTrigger value="painel">Painel</TabsTrigger>
<TabsTrigger value="lancamentos">Lançamentos</TabsTrigger>
</TabsList>
<PayerHeaderCard
payer={payerData}
selectedPeriod={selectedPeriod}
summary={summaryPreview}
/>
<LogoPrefetchProvider mappings={logoMappings}>
<Tabs defaultValue="profile" className="w-full">
<TabsList className="mb-2">
<TabsTrigger value="profile">Perfil</TabsTrigger>
<TabsTrigger value="painel">Painel</TabsTrigger>
<TabsTrigger value="lancamentos">Lançamentos</TabsTrigger>
</TabsList>
<PayerHeaderCard
payer={payerData}
selectedPeriod={selectedPeriod}
summary={summaryPreview}
/>
<TabsContent value="profile" className="space-y-4">
<PagadorInfoCard payer={payerData} />
{canEdit && payerData.shareCode ? (
<PayerSharingCard
payerId={pagador.id}
shareCode={payerData.shareCode}
shares={payerSharesData}
/>
) : null}
{!canEdit && currentUserShare ? (
<PayerLeaveShareCard
shareId={currentUserShare.id}
pagadorName={payerData.name}
createdAt={currentUserShare.createdAt}
/>
) : null}
</TabsContent>
<TabsContent value="profile" className="space-y-4">
<PagadorInfoCard payer={payerData} />
{canEdit && payerData.shareCode ? (
<PayerSharingCard
payerId={pagador.id}
shareCode={payerData.shareCode}
shares={payerSharesData}
/>
) : null}
{!canEdit && currentUserShare ? (
<PayerLeaveShareCard
shareId={currentUserShare.id}
pagadorName={payerData.name}
createdAt={currentUserShare.createdAt}
/>
) : null}
</TabsContent>
<TabsContent value="painel" className="space-y-4">
<section className="grid gap-3 lg:grid-cols-2">
<PayerMonthlySummaryCard
periodLabel={periodLabel}
breakdown={monthlyBreakdown}
/>
<PayerHistoryCard data={historyData} />
</section>
<TabsContent value="painel" className="space-y-4">
<section className="grid gap-3 lg:grid-cols-2">
<PayerMonthlySummaryCard
periodLabel={periodLabel}
breakdown={monthlyBreakdown}
/>
<PayerHistoryCard data={historyData} />
</section>
<section className="grid gap-3 lg:grid-cols-3">
<ExpandableWidgetCard
title="Minhas Faturas"
subtitle="Valores por cartão neste período"
icon={<RiBankCard2Line className="size-4" />}
>
<PayerCardUsageCard items={cardUsage} />
</ExpandableWidgetCard>
<ExpandableWidgetCard
title="Boletos"
subtitle="Boletos registrados neste período"
icon={<RiBarcodeLine className="size-4" />}
>
<PayerBoletoCard items={boletoItems} />
</ExpandableWidgetCard>
<ExpandableWidgetCard
title="Status de Pagamento"
subtitle="Situação das despesas no período"
icon={<RiWallet3Line className="size-4" />}
>
<PayerPaymentStatusCard data={paymentStatus} />
</ExpandableWidgetCard>
</section>
</TabsContent>
<section className="grid gap-3 lg:grid-cols-3">
<ExpandableWidgetCard
title="Minhas Faturas"
subtitle="Valores por cartão neste período"
icon={<RiBankCard2Line className="size-4" />}
>
<PayerCardUsageCard items={cardUsage} />
</ExpandableWidgetCard>
<ExpandableWidgetCard
title="Boletos"
subtitle="Boletos registrados neste período"
icon={<RiBarcodeLine className="size-4" />}
>
<PayerBoletoCard items={boletoItems} />
</ExpandableWidgetCard>
<ExpandableWidgetCard
title="Status de Pagamento"
subtitle="Situação das despesas no período"
icon={<RiWallet3Line className="size-4" />}
>
<PayerPaymentStatusCard data={paymentStatus} />
</ExpandableWidgetCard>
</section>
</TabsContent>
<TabsContent value="lancamentos">
<section className="flex flex-col gap-4">
<LancamentosSection
currentUserId={userId}
transactions={transactionData}
payerOptions={optionSets.payerOptions}
splitPayerOptions={optionSets.splitPayerOptions}
defaultPayerId={pagador.id}
accountOptions={optionSets.accountOptions}
cardOptions={optionSets.cardOptions}
categoryOptions={optionSets.categoryOptions}
payerFilterOptions={payerFilterOptions}
categoryFilterOptions={optionSets.categoryFilterOptions}
accountCardFilterOptions={optionSets.accountCardFilterOptions}
selectedPeriod={selectedPeriod}
estabelecimentos={estabelecimentos}
allowCreate={canEdit}
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
importPayerOptions={loggedUserOptionSets?.payerOptions}
importSplitPayerOptions={loggedUserOptionSets?.splitPayerOptions}
importDefaultPayerId={loggedUserOptionSets?.defaultPayerId}
importAccountOptions={loggedUserOptionSets?.accountOptions}
importCardOptions={loggedUserOptionSets?.cardOptions}
importCategoryOptions={loggedUserOptionSets?.categoryOptions}
/>
</section>
</TabsContent>
</Tabs>
<TabsContent value="lancamentos">
<section className="flex flex-col gap-4">
<LancamentosSection
currentUserId={userId}
transactions={transactionData}
payerOptions={optionSets.payerOptions}
splitPayerOptions={optionSets.splitPayerOptions}
defaultPayerId={pagador.id}
accountOptions={optionSets.accountOptions}
cardOptions={optionSets.cardOptions}
categoryOptions={optionSets.categoryOptions}
payerFilterOptions={payerFilterOptions}
categoryFilterOptions={optionSets.categoryFilterOptions}
accountCardFilterOptions={optionSets.accountCardFilterOptions}
selectedPeriod={selectedPeriod}
estabelecimentos={estabelecimentos}
allowCreate={canEdit}
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
importPayerOptions={loggedUserOptionSets?.payerOptions}
importSplitPayerOptions={
loggedUserOptionSets?.splitPayerOptions
}
importDefaultPayerId={loggedUserOptionSets?.defaultPayerId}
importAccountOptions={loggedUserOptionSets?.accountOptions}
importCardOptions={loggedUserOptionSets?.cardOptions}
importCategoryOptions={loggedUserOptionSets?.categoryOptions}
/>
</section>
</TabsContent>
</Tabs>
</LogoPrefetchProvider>
</main>
);
}

View File

@@ -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<ResolvedSearchParams>;
@@ -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 (
<main className="flex flex-col gap-6">
<MonthNavigation />
<TransactionsPage
currentUserId={userId}
transactions={transactionData}
payerOptions={payerOptions}
splitPayerOptions={splitPayerOptions}
defaultPayerId={defaultPayerId}
accountOptions={accountOptions}
cardOptions={cardOptions}
categoryOptions={categoryOptions}
payerFilterOptions={payerFilterOptions}
categoryFilterOptions={categoryFilterOptions}
accountCardFilterOptions={accountCardFilterOptions}
selectedPeriod={selectedPeriod}
estabelecimentos={estabelecimentos}
pagination={{
page: transactionsPage.page,
pageSize: transactionsPage.pageSize,
totalItems: transactionsPage.totalItems,
totalPages: transactionsPage.totalPages,
}}
exportContext={{
source: "transactions",
period: selectedPeriod,
filters: searchFilters,
}}
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
/>
<LogoPrefetchProvider mappings={logoMappings}>
<TransactionsPage
currentUserId={userId}
transactions={transactionData}
payerOptions={payerOptions}
splitPayerOptions={splitPayerOptions}
defaultPayerId={defaultPayerId}
accountOptions={accountOptions}
cardOptions={cardOptions}
categoryOptions={categoryOptions}
payerFilterOptions={payerFilterOptions}
categoryFilterOptions={categoryFilterOptions}
accountCardFilterOptions={accountCardFilterOptions}
selectedPeriod={selectedPeriod}
estabelecimentos={estabelecimentos}
pagination={{
page: transactionsPage.page,
pageSize: transactionsPage.pageSize,
totalItems: transactionsPage.totalItems,
totalPages: transactionsPage.totalPages,
}}
exportContext={{
source: "transactions",
period: selectedPeriod,
filters: searchFilters,
}}
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
/>
</LogoPrefetchProvider>
</main>
);
}