chore(cleanup): remove dead code and legacy top-estabelecimentos route
This commit is contained in:
15
CHANGELOG.md
15
CHANGELOG.md
@@ -13,10 +13,17 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
- Tabela `passkey` no banco de dados para persistência de credenciais WebAuthn vinculadas ao usuário
|
- Tabela `passkey` no banco de dados para persistência de credenciais WebAuthn vinculadas ao usuário
|
||||||
- Nova aba **Passkeys** em `/ajustes` com gerenciamento de credenciais: listar, adicionar, renomear e remover passkeys
|
- Nova aba **Passkeys** em `/ajustes` com gerenciamento de credenciais: listar, adicionar, renomear e remover passkeys
|
||||||
- Ação de login com passkey na tela de autenticação (`/login`)
|
- Ação de login com passkey na tela de autenticação (`/login`)
|
||||||
|
- Dashboard: botões rápidos na toolbar de widgets para `Nova receita`, `Nova despesa` e `Nova anotação` com abertura direta dos diálogos correspondentes
|
||||||
|
- Widget de **Anotações** no dashboard com listagem das anotações ativas, ações discretas de editar e ver detalhes, e atalho para `/anotacoes`
|
||||||
|
|
||||||
### Alterado
|
### Alterado
|
||||||
|
|
||||||
- `PasskeysForm` refatorado para melhor experiência com React 19/Next 16: detecção de suporte do navegador, bloqueio de ações simultâneas e atualização da lista sem loader global após operações
|
- `PasskeysForm` refatorado para melhor experiência com React 19/Next 16: detecção de suporte do navegador, bloqueio de ações simultâneas e atualização da lista sem loader global após operações
|
||||||
|
- Widget de pagadores no dashboard agora exibe variação percentual em relação ao mês anterior (seta + cor semântica), seguindo o padrão visual dos widgets de categorias
|
||||||
|
- Dashboard: widgets `Condições de Pagamentos` + `Formas de Pagamento` unificados em um único widget com abas; `Top Estabelecimentos` + `Maiores Gastos do Mês` também unificados em widget com abas
|
||||||
|
- Relatórios: rota de Top Estabelecimentos consolidada em `/relatorios/estabelecimentos`
|
||||||
|
- Dashboard: widget `Lançamentos recentes` removido e substituído por `Progresso de metas` com lista de orçamentos do período (gasto, limite configurado e percentual de uso por categoria)
|
||||||
|
- Dashboard: `fetchDashboardData` deixou de carregar `notificationsSnapshot` (notificações continuam sendo carregadas no layout), reduzindo uma query no carregamento da página inicial
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|
||||||
@@ -24,6 +31,14 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
|||||||
- Listagem de passkeys em Ajustes agora trata `createdAt` ausente sem gerar data inválida na interface
|
- Listagem de passkeys em Ajustes agora trata `createdAt` ausente sem gerar data inválida na interface
|
||||||
- Migração `0017_previous_warstar` tornou-se idempotente para colunas de `preferencias_usuario` com `IF NOT EXISTS`, evitando falha em bancos já migrados
|
- Migração `0017_previous_warstar` tornou-se idempotente para colunas de `preferencias_usuario` com `IF NOT EXISTS`, evitando falha em bancos já migrados
|
||||||
|
|
||||||
|
### Removido
|
||||||
|
|
||||||
|
- Código legado não utilizado no dashboard: widget e fetcher de `Lançamentos Recentes`
|
||||||
|
- Componente legado `CategoryCard` em categorias (substituído pelo layout atual em tabela)
|
||||||
|
- Componente `AuthFooter` não utilizado na autenticação
|
||||||
|
- Barrel files sem consumo em `components/relatorios`, `components/lancamentos` e `components/lancamentos/shared`
|
||||||
|
- Rota legada `/top-estabelecimentos` e arquivos auxiliares (`layout.tsx` e `loading.tsx`) removidos
|
||||||
|
|
||||||
## [1.7.5] - 2026-02-28
|
## [1.7.5] - 2026-02-28
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { RiStore2Line } from "@remixicon/react";
|
|
||||||
import PageDescription from "@/components/page-description";
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: "Top Estabelecimentos | OpenMonetis",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<section className="space-y-6 pt-4">
|
|
||||||
<PageDescription
|
|
||||||
icon={<RiStore2Line />}
|
|
||||||
title="Top Estabelecimentos"
|
|
||||||
subtitle="Análise dos locais onde você mais compra e gasta"
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<main className="flex flex-col gap-4 px-6">
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<Skeleton className="h-8 w-48" />
|
|
||||||
<Skeleton className="h-4 w-64" />
|
|
||||||
</div>
|
|
||||||
<Skeleton className="h-8 w-48" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{[1, 2, 3, 4].map((i) => (
|
|
||||||
<Card key={i}>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<Skeleton className="h-16 w-full" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<Skeleton className="h-20 w-full" />
|
|
||||||
<Skeleton className="h-20 w-full" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 lg:grid-cols-3">
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<Skeleton className="h-5 w-48" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
|
||||||
<Skeleton key={i} className="h-16 w-full" />
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<Skeleton className="h-5 w-40" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
{[1, 2, 3, 4, 5].map((i) => (
|
|
||||||
<Skeleton key={i} className="h-12 w-full" />
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { EstablishmentsList } from "@/components/top-estabelecimentos/establishments-list";
|
|
||||||
import { HighlightsCards } from "@/components/top-estabelecimentos/highlights-cards";
|
|
||||||
import { PeriodFilterButtons } from "@/components/top-estabelecimentos/period-filter";
|
|
||||||
import { SummaryCards } from "@/components/top-estabelecimentos/summary-cards";
|
|
||||||
import { TopCategories } from "@/components/top-estabelecimentos/top-categories";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { getUser } from "@/lib/auth/server";
|
|
||||||
import {
|
|
||||||
fetchTopEstabelecimentosData,
|
|
||||||
type PeriodFilter,
|
|
||||||
} from "@/lib/top-estabelecimentos/fetch-data";
|
|
||||||
import { parsePeriodParam } from "@/lib/utils/period";
|
|
||||||
|
|
||||||
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
|
|
||||||
|
|
||||||
type PageProps = {
|
|
||||||
searchParams?: PageSearchParams;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSingleParam = (
|
|
||||||
params: Record<string, string | string[] | undefined> | undefined,
|
|
||||||
key: string,
|
|
||||||
) => {
|
|
||||||
const value = params?.[key];
|
|
||||||
if (!value) return null;
|
|
||||||
return Array.isArray(value) ? (value[0] ?? null) : value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validatePeriodFilter = (value: string | null): PeriodFilter => {
|
|
||||||
if (value === "3" || value === "6" || value === "12") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return "6";
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function TopEstabelecimentosPage({
|
|
||||||
searchParams,
|
|
||||||
}: PageProps) {
|
|
||||||
const user = await getUser();
|
|
||||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
|
||||||
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
|
||||||
const mesesParam = getSingleParam(resolvedSearchParams, "meses");
|
|
||||||
|
|
||||||
const { period: currentPeriod } = parsePeriodParam(periodoParam);
|
|
||||||
const periodFilter = validatePeriodFilter(mesesParam);
|
|
||||||
|
|
||||||
const data = await fetchTopEstabelecimentosData(
|
|
||||||
user.id,
|
|
||||||
currentPeriod,
|
|
||||||
periodFilter,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="flex flex-col gap-4">
|
|
||||||
<Card className="p-3 flex-row justify-between items-center">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Selecione o intervalo de meses
|
|
||||||
</span>
|
|
||||||
<PeriodFilterButtons currentFilter={periodFilter} />
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<SummaryCards summary={data.summary} />
|
|
||||||
|
|
||||||
<HighlightsCards summary={data.summary} />
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<EstablishmentsList establishments={data.establishments} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TopCategories categories={data.topCategories} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,6 @@ export default function robots(): MetadataRoute.Robots {
|
|||||||
"/consultor",
|
"/consultor",
|
||||||
"/ajustes",
|
"/ajustes",
|
||||||
"/relatorios",
|
"/relatorios",
|
||||||
"/top-estabelecimentos",
|
|
||||||
"/pre-lancamentos",
|
"/pre-lancamentos",
|
||||||
"/login",
|
"/login",
|
||||||
"/api/",
|
"/api/",
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { FieldDescription } from "@/components/ui/field";
|
|
||||||
|
|
||||||
export function AuthFooter() {
|
|
||||||
return (
|
|
||||||
<FieldDescription className="px-6 text-center">
|
|
||||||
Ao continuar, você concorda com nossos{" "}
|
|
||||||
<a href="/terms" className="underline underline-offset-4">
|
|
||||||
Termos de Serviço
|
|
||||||
</a>{" "}
|
|
||||||
e{" "}
|
|
||||||
<a href="/privacy" className="underline underline-offset-4">
|
|
||||||
Política de Privacidade
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</FieldDescription>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -255,7 +255,6 @@ export function LoginForm({ className, ...props }: DivProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* <AuthFooter /> */}
|
|
||||||
<FieldDescription className="text-center">
|
<FieldDescription className="text-center">
|
||||||
<a href="/" className="underline underline-offset-4">
|
<a href="/" className="underline underline-offset-4">
|
||||||
Voltar para o site
|
Voltar para o site
|
||||||
|
|||||||
@@ -281,7 +281,6 @@ export function SignupForm({ className, ...props }: DivProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* <AuthFooter /> */}
|
|
||||||
<FieldDescription className="text-center">
|
<FieldDescription className="text-center">
|
||||||
<a href="/" className="underline underline-offset-4">
|
<a href="/" className="underline underline-offset-4">
|
||||||
Voltar para o site
|
Voltar para o site
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
RiDeleteBin5Line,
|
|
||||||
RiFileList2Line,
|
|
||||||
RiPencilLine,
|
|
||||||
} from "@remixicon/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
|
||||||
import { cn } from "@/lib/utils/ui";
|
|
||||||
import { CategoryIconBadge } from "./category-icon-badge";
|
|
||||||
import type { Category } from "./types";
|
|
||||||
|
|
||||||
interface CategoryCardProps {
|
|
||||||
category: Category;
|
|
||||||
colorIndex: number;
|
|
||||||
onEdit: (category: Category) => void;
|
|
||||||
onRemove: (category: Category) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CategoryCard({
|
|
||||||
category,
|
|
||||||
colorIndex,
|
|
||||||
onEdit,
|
|
||||||
onRemove,
|
|
||||||
}: CategoryCardProps) {
|
|
||||||
const categoriasProtegidas = [
|
|
||||||
"Transferência interna",
|
|
||||||
"Saldo inicial",
|
|
||||||
"Pagamentos",
|
|
||||||
];
|
|
||||||
const isProtegida = categoriasProtegidas.includes(category.name);
|
|
||||||
|
|
||||||
const actions = [
|
|
||||||
{
|
|
||||||
label: "editar",
|
|
||||||
icon: <RiPencilLine className="size-4" aria-hidden />,
|
|
||||||
onClick: () => onEdit(category),
|
|
||||||
variant: "default" as const,
|
|
||||||
disabled: isProtegida,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "detalhes",
|
|
||||||
icon: <RiFileList2Line className="size-4" aria-hidden />,
|
|
||||||
href: `/categorias/${category.id}`,
|
|
||||||
variant: "default" as const,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "remover",
|
|
||||||
icon: <RiDeleteBin5Line className="size-4" aria-hidden />,
|
|
||||||
onClick: () => onRemove(category),
|
|
||||||
variant: "destructive" as const,
|
|
||||||
disabled: isProtegida,
|
|
||||||
},
|
|
||||||
].filter((action) => !action.disabled);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="flex h-full flex-col gap-0 py-3">
|
|
||||||
<CardContent className="flex flex-1 flex-col">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<CategoryIconBadge
|
|
||||||
icon={category.icon}
|
|
||||||
name={category.name}
|
|
||||||
colorIndex={colorIndex}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
<h3 className="leading-tight">{category.name}</h3>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
<CardFooter className="flex flex-wrap gap-3 px-6 pt-4 text-sm">
|
|
||||||
{actions.map(({ label, icon, onClick, href, variant }) => {
|
|
||||||
const className = cn(
|
|
||||||
"flex items-center gap-1 font-medium transition-opacity hover:opacity-80",
|
|
||||||
variant === "destructive" ? "text-destructive" : "text-primary",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (href) {
|
|
||||||
return (
|
|
||||||
<Link key={label} href={href} className={className}>
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={label}
|
|
||||||
type="button"
|
|
||||||
onClick={onClick}
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { RiExchangeLine } from "@remixicon/react";
|
|
||||||
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
|
|
||||||
import MoneyValues from "@/components/money-values";
|
|
||||||
import type { RecentTransactionsData } from "@/lib/dashboard/recent-transactions";
|
|
||||||
import { WidgetEmptyState } from "../widget-empty-state";
|
|
||||||
|
|
||||||
type RecentTransactionsWidgetProps = {
|
|
||||||
data: RecentTransactionsData;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTransactionDate = (date: Date | string) => {
|
|
||||||
const d = date instanceof Date ? date : new Date(date);
|
|
||||||
const formatter = new Intl.DateTimeFormat("pt-BR", {
|
|
||||||
weekday: "short",
|
|
||||||
day: "2-digit",
|
|
||||||
month: "short",
|
|
||||||
timeZone: "UTC",
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatted = formatter.format(d);
|
|
||||||
// Capitaliza a primeira letra do dia da semana
|
|
||||||
return formatted.charAt(0).toUpperCase() + formatted.slice(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function RecentTransactionsWidget({
|
|
||||||
data,
|
|
||||||
}: RecentTransactionsWidgetProps) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col px-0">
|
|
||||||
{data.transactions.length === 0 ? (
|
|
||||||
<WidgetEmptyState
|
|
||||||
icon={<RiExchangeLine className="size-6 text-muted-foreground" />}
|
|
||||||
title="Nenhum lançamento encontrado"
|
|
||||||
description="Quando houver despesas registradas, elas aparecerão aqui."
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ul className="flex flex-col">
|
|
||||||
{data.transactions.map((transaction) => {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={transaction.id}
|
|
||||||
className="flex items-center justify-between gap-3 border-b border-dashed py-2 last:border-b-0 last:pb-0"
|
|
||||||
>
|
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
||||||
<EstabelecimentoLogo name={transaction.name} size={37} />
|
|
||||||
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="truncate text-sm font-medium text-foreground">
|
|
||||||
{transaction.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{formatTransactionDate(transaction.purchaseDate)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="shrink-0 text-foreground">
|
|
||||||
<MoneyValues amount={transaction.amount} />
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// Main page component
|
|
||||||
|
|
||||||
export { default as AnticipateInstallmentsDialog } from "./dialogs/anticipate-installments-dialog/anticipate-installments-dialog";
|
|
||||||
export { default as BulkActionDialog } from "./dialogs/bulk-action-dialog";
|
|
||||||
export { default as LancamentoDetailsDialog } from "./dialogs/lancamento-details-dialog";
|
|
||||||
|
|
||||||
// Main dialogs
|
|
||||||
export { default as LancamentoDialog } from "./dialogs/lancamento-dialog/lancamento-dialog";
|
|
||||||
export type * from "./dialogs/lancamento-dialog/lancamento-dialog-types";
|
|
||||||
export { default as MassAddDialog } from "./dialogs/mass-add-dialog";
|
|
||||||
export { default as LancamentosPage } from "./page/lancamentos-page";
|
|
||||||
export * from "./select-items";
|
|
||||||
export { default as AnticipationCard } from "./shared/anticipation-card";
|
|
||||||
// Shared components
|
|
||||||
export { default as EstabelecimentoInput } from "./shared/estabelecimento-input";
|
|
||||||
export { default as InstallmentTimeline } from "./shared/installment-timeline";
|
|
||||||
export { default as LancamentosFilters } from "./table/lancamentos-filters";
|
|
||||||
// Table components
|
|
||||||
export { default as LancamentosTable } from "./table/lancamentos-table";
|
|
||||||
// Types and utilities
|
|
||||||
export type * from "./types";
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export { AnticipationCard } from "./anticipation-card";
|
|
||||||
export { EstabelecimentoInput } from "./estabelecimento-input";
|
|
||||||
export { EstabelecimentoLogo } from "./estabelecimento-logo";
|
|
||||||
export { InstallmentTimeline } from "./installment-timeline";
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export { CategoryCell } from "./category-cell";
|
|
||||||
export { CategoryReportCards } from "./category-report-cards";
|
|
||||||
export { CategoryReportChart } from "./category-report-chart";
|
|
||||||
export { CategoryReportExport } from "./category-report-export";
|
|
||||||
export { CategoryReportFilters } from "./category-report-filters";
|
|
||||||
export { CategoryReportPage } from "./category-report-page";
|
|
||||||
export { CategoryReportTable } from "./category-report-table";
|
|
||||||
export { CategoryTable } from "./category-table";
|
|
||||||
export type {
|
|
||||||
CategoryOption,
|
|
||||||
CategoryReportFiltersProps,
|
|
||||||
FilterState,
|
|
||||||
} from "./types";
|
|
||||||
@@ -6,16 +6,16 @@ import { fetchIncomeByCategory } from "./categories/income-by-category";
|
|||||||
import { fetchInstallmentExpenses } from "./expenses/installment-expenses";
|
import { fetchInstallmentExpenses } from "./expenses/installment-expenses";
|
||||||
import { fetchRecurringExpenses } from "./expenses/recurring-expenses";
|
import { fetchRecurringExpenses } from "./expenses/recurring-expenses";
|
||||||
import { fetchTopExpenses } from "./expenses/top-expenses";
|
import { fetchTopExpenses } from "./expenses/top-expenses";
|
||||||
|
import { fetchGoalsProgressData } from "./goals-progress";
|
||||||
import { fetchIncomeExpenseBalance } from "./income-expense-balance";
|
import { fetchIncomeExpenseBalance } from "./income-expense-balance";
|
||||||
import { fetchDashboardInvoices } from "./invoices";
|
import { fetchDashboardInvoices } from "./invoices";
|
||||||
import { fetchDashboardCardMetrics } from "./metrics";
|
import { fetchDashboardCardMetrics } from "./metrics";
|
||||||
import { fetchDashboardNotifications } from "./notifications";
|
import { fetchDashboardNotes } from "./notes";
|
||||||
import { fetchDashboardPagadores } from "./pagadores";
|
import { fetchDashboardPagadores } from "./pagadores";
|
||||||
import { fetchPaymentConditions } from "./payments/payment-conditions";
|
import { fetchPaymentConditions } from "./payments/payment-conditions";
|
||||||
import { fetchPaymentMethods } from "./payments/payment-methods";
|
import { fetchPaymentMethods } from "./payments/payment-methods";
|
||||||
import { fetchPaymentStatus } from "./payments/payment-status";
|
import { fetchPaymentStatus } from "./payments/payment-status";
|
||||||
import { fetchPurchasesByCategory } from "./purchases-by-category";
|
import { fetchPurchasesByCategory } from "./purchases-by-category";
|
||||||
import { fetchRecentTransactions } from "./recent-transactions";
|
|
||||||
import { fetchTopEstablishments } from "./top-establishments";
|
import { fetchTopEstablishments } from "./top-establishments";
|
||||||
|
|
||||||
async function fetchDashboardDataInternal(userId: string, period: string) {
|
async function fetchDashboardDataInternal(userId: string, period: string) {
|
||||||
@@ -24,11 +24,11 @@ async function fetchDashboardDataInternal(userId: string, period: string) {
|
|||||||
accountsSnapshot,
|
accountsSnapshot,
|
||||||
invoicesSnapshot,
|
invoicesSnapshot,
|
||||||
boletosSnapshot,
|
boletosSnapshot,
|
||||||
notificationsSnapshot,
|
goalsProgressData,
|
||||||
paymentStatusData,
|
paymentStatusData,
|
||||||
incomeExpenseBalanceData,
|
incomeExpenseBalanceData,
|
||||||
pagadoresSnapshot,
|
pagadoresSnapshot,
|
||||||
recentTransactionsData,
|
notesData,
|
||||||
paymentConditionsData,
|
paymentConditionsData,
|
||||||
paymentMethodsData,
|
paymentMethodsData,
|
||||||
recurringExpensesData,
|
recurringExpensesData,
|
||||||
@@ -44,11 +44,11 @@ async function fetchDashboardDataInternal(userId: string, period: string) {
|
|||||||
fetchDashboardAccounts(userId),
|
fetchDashboardAccounts(userId),
|
||||||
fetchDashboardInvoices(userId, period),
|
fetchDashboardInvoices(userId, period),
|
||||||
fetchDashboardBoletos(userId, period),
|
fetchDashboardBoletos(userId, period),
|
||||||
fetchDashboardNotifications(userId, period),
|
fetchGoalsProgressData(userId, period),
|
||||||
fetchPaymentStatus(userId, period),
|
fetchPaymentStatus(userId, period),
|
||||||
fetchIncomeExpenseBalance(userId, period),
|
fetchIncomeExpenseBalance(userId, period),
|
||||||
fetchDashboardPagadores(userId, period),
|
fetchDashboardPagadores(userId, period),
|
||||||
fetchRecentTransactions(userId, period),
|
fetchDashboardNotes(userId),
|
||||||
fetchPaymentConditions(userId, period),
|
fetchPaymentConditions(userId, period),
|
||||||
fetchPaymentMethods(userId, period),
|
fetchPaymentMethods(userId, period),
|
||||||
fetchRecurringExpenses(userId, period),
|
fetchRecurringExpenses(userId, period),
|
||||||
@@ -66,11 +66,11 @@ async function fetchDashboardDataInternal(userId: string, period: string) {
|
|||||||
accountsSnapshot,
|
accountsSnapshot,
|
||||||
invoicesSnapshot,
|
invoicesSnapshot,
|
||||||
boletosSnapshot,
|
boletosSnapshot,
|
||||||
notificationsSnapshot,
|
goalsProgressData,
|
||||||
paymentStatusData,
|
paymentStatusData,
|
||||||
incomeExpenseBalanceData,
|
incomeExpenseBalanceData,
|
||||||
pagadoresSnapshot,
|
pagadoresSnapshot,
|
||||||
recentTransactionsData,
|
notesData,
|
||||||
paymentConditionsData,
|
paymentConditionsData,
|
||||||
paymentMethodsData,
|
paymentMethodsData,
|
||||||
recurringExpensesData,
|
recurringExpensesData,
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import { and, desc, eq, isNull, or, sql } from "drizzle-orm";
|
|
||||||
import { cartoes, contas, lancamentos } from "@/db/schema";
|
|
||||||
import {
|
|
||||||
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
|
|
||||||
INITIAL_BALANCE_NOTE,
|
|
||||||
} from "@/lib/accounts/constants";
|
|
||||||
import { toNumber } from "@/lib/dashboard/common";
|
|
||||||
import { db } from "@/lib/db";
|
|
||||||
import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id";
|
|
||||||
|
|
||||||
export type RecentTransaction = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
amount: number;
|
|
||||||
purchaseDate: Date;
|
|
||||||
cardLogo: string | null;
|
|
||||||
accountLogo: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RecentTransactionsData = {
|
|
||||||
transactions: RecentTransaction[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function fetchRecentTransactions(
|
|
||||||
userId: string,
|
|
||||||
period: string,
|
|
||||||
): Promise<RecentTransactionsData> {
|
|
||||||
const adminPagadorId = await getAdminPagadorId(userId);
|
|
||||||
if (!adminPagadorId) {
|
|
||||||
return { transactions: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await db
|
|
||||||
.select({
|
|
||||||
id: lancamentos.id,
|
|
||||||
name: lancamentos.name,
|
|
||||||
amount: lancamentos.amount,
|
|
||||||
purchaseDate: lancamentos.purchaseDate,
|
|
||||||
cardLogo: cartoes.logo,
|
|
||||||
accountLogo: contas.logo,
|
|
||||||
note: lancamentos.note,
|
|
||||||
})
|
|
||||||
.from(lancamentos)
|
|
||||||
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id))
|
|
||||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(lancamentos.userId, userId),
|
|
||||||
eq(lancamentos.period, period),
|
|
||||||
eq(lancamentos.transactionType, "Despesa"),
|
|
||||||
eq(lancamentos.pagadorId, adminPagadorId),
|
|
||||||
or(
|
|
||||||
isNull(lancamentos.note),
|
|
||||||
and(
|
|
||||||
sql`${lancamentos.note} != ${INITIAL_BALANCE_NOTE}`,
|
|
||||||
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.orderBy(desc(lancamentos.purchaseDate), desc(lancamentos.createdAt))
|
|
||||||
.limit(5);
|
|
||||||
|
|
||||||
const transactions = results.map((row): RecentTransaction => {
|
|
||||||
return {
|
|
||||||
id: row.id,
|
|
||||||
name: row.name,
|
|
||||||
amount: Math.abs(toNumber(row.amount)),
|
|
||||||
purchaseDate: row.purchaseDate,
|
|
||||||
cardLogo: row.cardLogo,
|
|
||||||
accountLogo: row.accountLogo,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
transactions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user