diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cbfcf5..8ffd5e4 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.5.3] - 2026-02-21 + +### Adicionado + +- Painel do pagador: card "Status de Pagamento" com totais pagos/pendentes e listagem individual de boletos com data de vencimento, data de pagamento e status +- Funções `fetchPagadorBoletoItems` e `fetchPagadorPaymentStatus` em `lib/pagadores/details.ts` +- SEO completo na landing page: metadata Open Graph, Twitter Card, JSON-LD Schema.org, sitemap.xml (`/app/sitemap.ts`) e robots.txt (`/app/robots.ts`) +- Layout específico da landing page (`app/(landing-page)/layout.tsx`) com metadados ricos + +### Corrigido + +- Validação obrigatória de categoria, conta e cartão no dialog de lançamento — agora validada no cliente (antes do submit) e no servidor via Zod +- Atributo `lang` do HTML corrigido de `en` para `pt-BR` + +### Alterado + +- Painel do pagador reorganizado em grid de 3 colunas com cards de Faturas, Boletos e Status de Pagamento +- `PagadorBoletoCard` refatorado para exibir lista de boletos individuais em vez de resumo agregado +- Imagens da landing page convertidas de PNG para WebP (melhora de performance) +- Template de título dinâmico no layout raiz (`%s | OpenMonetis`) + ## [1.5.2] - 2026-02-16 ### Alterado diff --git a/app/(dashboard)/lancamentos/actions.ts b/app/(dashboard)/lancamentos/actions.ts index 1bcdb05..b6364a8 100644 --- a/app/(dashboard)/lancamentos/actions.ts +++ b/app/(dashboard)/lancamentos/actions.ts @@ -182,6 +182,30 @@ const refineLancamento = ( data: z.infer & { id?: string }, ctx: z.RefinementCtx, ) => { + if (!data.categoriaId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["categoriaId"], + message: "Selecione uma categoria.", + }); + } + + if (data.paymentMethod === "Cartão de crédito") { + if (!data.cartaoId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["cartaoId"], + message: "Selecione o cartão.", + }); + } + } else if (!data.contaId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["contaId"], + message: "Selecione a conta.", + }); + } + if (data.condition === "Parcelado") { if (!data.installmentCount) { ctx.addIssue({ diff --git a/app/(dashboard)/pagadores/[pagadorId]/page.tsx b/app/(dashboard)/pagadores/[pagadorId]/page.tsx index 462ba81..ddc7866 100644 --- a/app/(dashboard)/pagadores/[pagadorId]/page.tsx +++ b/app/(dashboard)/pagadores/[pagadorId]/page.tsx @@ -1,3 +1,8 @@ +import { + RiBankCard2Line, + RiBarcodeLine, + RiWallet3Line, +} from "@remixicon/react"; import { notFound } from "next/navigation"; import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions"; import { LancamentosPage as LancamentosSection } from "@/components/lancamentos/page/lancamentos-page"; @@ -13,9 +18,13 @@ import { PagadorHistoryCard } from "@/components/pagadores/details/pagador-histo import { PagadorInfoCard } from "@/components/pagadores/details/pagador-info-card"; import { PagadorLeaveShareCard } from "@/components/pagadores/details/pagador-leave-share-card"; import { PagadorMonthlySummaryCard } from "@/components/pagadores/details/pagador-monthly-summary-card"; -import { PagadorBoletoCard } from "@/components/pagadores/details/pagador-payment-method-cards"; +import { + PagadorBoletoCard, + PagadorPaymentStatusCard, +} from "@/components/pagadores/details/pagador-payment-method-cards"; import { PagadorSharingCard } from "@/components/pagadores/details/pagador-sharing-card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import WidgetCard from "@/components/widget-card"; import type { pagadores } from "@/db/schema"; import { getUserId } from "@/lib/auth/server"; import { @@ -34,10 +43,12 @@ import { } from "@/lib/lancamentos/page-helpers"; import { getPagadorAccess } from "@/lib/pagadores/access"; import { + fetchPagadorBoletoItems, fetchPagadorBoletoStats, fetchPagadorCardUsage, fetchPagadorHistory, fetchPagadorMonthlyBreakdown, + fetchPagadorPaymentStatus, } from "@/lib/pagadores/details"; import { parsePeriodParam } from "@/lib/utils/period"; import { @@ -152,6 +163,8 @@ export default async function Page({ params, searchParams }: PageProps) { historyData, cardUsage, boletoStats, + boletoItems, + paymentStatus, shareRows, currentUserShare, estabelecimentos, @@ -177,6 +190,16 @@ export default async function Page({ params, searchParams }: PageProps) { pagadorId: pagador.id, period: selectedPeriod, }), + fetchPagadorBoletoItems({ + userId: dataOwnerId, + pagadorId: pagador.id, + period: selectedPeriod, + }), + fetchPagadorPaymentStatus({ + userId: dataOwnerId, + pagadorId: pagador.id, + period: selectedPeriod, + }), sharesPromise, currentUserSharePromise, getRecentEstablishmentsAction(), @@ -308,7 +331,7 @@ export default async function Page({ params, searchParams }: PageProps) { -
+
-
- - +
+ } + > + + + } + > + + + } + > + +
diff --git a/app/(landing-page)/layout.tsx b/app/(landing-page)/layout.tsx new file mode 100644 index 0000000..d52d6a0 --- /dev/null +++ b/app/(landing-page)/layout.tsx @@ -0,0 +1,95 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +const BASE_URL = process.env.PUBLIC_DOMAIN + ? `https://${process.env.PUBLIC_DOMAIN}` + : "https://openmonetis.com"; + +const TITLE = "OpenMonetis | Finanças pessoais self-hosted e open source"; +const DESCRIPTION = + "Aplicativo self-hosted de finanças pessoais. Controle lançamentos, cartões, orçamentos e categorias com total privacidade. Open source e gratuito."; + +export const metadata: Metadata = { + metadataBase: new URL(BASE_URL), + title: TITLE, + description: DESCRIPTION, + keywords: [ + "finanças pessoais", + "controle financeiro", + "self-hosted", + "open source", + "gestão financeira", + "orçamento pessoal", + "lançamentos financeiros", + "cartão de crédito", + "planejamento financeiro", + ], + alternates: { + canonical: "/", + }, + openGraph: { + type: "website", + locale: "pt_BR", + url: "/", + siteName: "OpenMonetis", + title: TITLE, + description: DESCRIPTION, + images: [ + { + url: "/dashboard-preview-light.webp", + width: 1920, + height: 1080, + alt: "OpenMonetis — Dashboard de finanças pessoais", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: TITLE, + description: DESCRIPTION, + images: ["/dashboard-preview-light.webp"], + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-image-preview": "large", + "max-snippet": -1, + }, + }, +}; + +export default function LandingLayout({ children }: { children: ReactNode }) { + const jsonLd = { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + name: "OpenMonetis", + applicationCategory: "FinanceApplication", + operatingSystem: "Web", + offers: { + "@type": "Offer", + price: "0", + priceCurrency: "BRL", + }, + description: DESCRIPTION, + url: BASE_URL, + isAccessibleForFree: true, + author: { + "@type": "Organization", + name: "OpenMonetis", + url: BASE_URL, + }, + }; + + return ( + <> +