diff --git a/src/app/(dashboard)/attachments/layout.tsx b/src/app/(dashboard)/attachments/layout.tsx
new file mode 100644
index 0000000..b2c6ae7
--- /dev/null
+++ b/src/app/(dashboard)/attachments/layout.tsx
@@ -0,0 +1,26 @@
+import { RiAttachmentLine } from "@remixicon/react";
+import MonthNavigation from "@/shared/components/month-picker/month-navigation";
+import PageDescription from "@/shared/components/page-description";
+
+export const metadata = {
+ title: "Anexos",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ }
+ title="Anexos"
+ subtitle="Gerencie os anexos das suas transações"
+ />
+
+
+ {children}
+
+ );
+}
diff --git a/src/features/dashboard/components/attachments-widget.tsx b/src/features/dashboard/components/attachments-widget.tsx
new file mode 100644
index 0000000..e78731c
--- /dev/null
+++ b/src/features/dashboard/components/attachments-widget.tsx
@@ -0,0 +1,128 @@
+"use client";
+
+import {
+ RiAttachmentLine,
+ RiFileLine,
+ RiFilePdf2Line,
+ RiImageLine,
+} from "@remixicon/react";
+import { useState } from "react";
+import { AttachmentPreview } from "@/features/attachments/components/attachment-preview";
+import type { AttachmentForPeriod } from "@/features/attachments/queries";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/shared/components/ui/tooltip";
+import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
+import { formatDateOnly } from "@/shared/utils/date";
+import { formatBytes } from "@/shared/utils/number";
+
+type AttachmentsSnapshot = {
+ totalCount: number;
+ totalBytes: number;
+ imageCount: number;
+ pdfCount: number;
+ recentAttachments: AttachmentForPeriod[];
+};
+
+type AttachmentsWidgetProps = {
+ snapshot: AttachmentsSnapshot;
+};
+
+export function AttachmentsWidget({ snapshot }: AttachmentsWidgetProps) {
+ const [selectedIndex, setSelectedIndex] = useState(-1);
+
+ if (snapshot.totalCount === 0) {
+ return (
+ }
+ title="Nenhum anexo no período"
+ description="Adicione comprovantes nos seus lançamentos para vê-los aqui."
+ />
+ );
+ }
+
+ return (
+ <>
+
+
+
+ {snapshot.totalCount} {snapshot.totalCount === 1 ? "anexo" : "anexos"}
+
+
+ {formatBytes(snapshot.totalBytes)}
+
+ {snapshot.imageCount > 0 && (
+
+
+ {snapshot.imageCount}
+
+ )}
+ {snapshot.pdfCount > 0 && (
+
+
+ {snapshot.pdfCount}
+
+ )}
+
+
+
+ {snapshot.recentAttachments.map((attachment, index) => {
+ const isPdf = attachment.mimeType === "application/pdf";
+ const isImage = attachment.mimeType.startsWith("image/");
+
+ return (
+ -
+
+
+ );
+ })}
+
+
+ setSelectedIndex(-1)}
+ />
+ >
+ );
+}
diff --git a/src/features/dashboard/components/category-trends-widget.tsx b/src/features/dashboard/components/category-trends-widget.tsx
new file mode 100644
index 0000000..4459ef5
--- /dev/null
+++ b/src/features/dashboard/components/category-trends-widget.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import {
+ RiArrowDownSFill,
+ RiArrowUpSFill,
+ RiLineChartLine,
+} from "@remixicon/react";
+import type { DashboardCategoryBreakdownItem } from "@/features/dashboard/categories/category-breakdown";
+import { CategoryIconBadge } from "@/shared/components/entity-avatar";
+import MoneyValues from "@/shared/components/money-values";
+import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
+import { cn } from "@/shared/utils/ui";
+
+type CategoryTrendsWidgetProps = {
+ categories: DashboardCategoryBreakdownItem[];
+};
+
+export function CategoryTrendsWidget({
+ categories,
+}: CategoryTrendsWidgetProps) {
+ const trending = categories
+ .filter((c) => c.percentageChange !== null && c.previousAmount > 0)
+ .sort(
+ (a, b) =>
+ Math.abs(b.percentageChange ?? 0) - Math.abs(a.percentageChange ?? 0),
+ )
+ .slice(0, 6);
+
+ if (trending.length === 0) {
+ return (
+ }
+ title="Dados insuficientes"
+ description="As variações aparecem após lançamentos em dois meses consecutivos."
+ />
+ );
+ }
+
+ return (
+
+ {trending.map((category) => {
+ const change = category.percentageChange ?? 0;
+ const isUp = change > 0;
+
+ return (
+ -
+
+
+
+
+ {category.categoryName}
+
+
+ vs{" "}
+
+
+
+
+ {isUp ? (
+
+ ) : (
+
+ )}
+ {Math.abs(change).toFixed(0)}%
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/features/dashboard/components/dashboard-grid-editable.tsx b/src/features/dashboard/components/dashboard-grid-editable.tsx
index 5b2abe4..a6b768d 100644
--- a/src/features/dashboard/components/dashboard-grid-editable.tsx
+++ b/src/features/dashboard/components/dashboard-grid-editable.tsx
@@ -34,12 +34,12 @@ import {
type WidgetPreferences,
} from "@/features/dashboard/widgets/actions";
import {
+ type DashboardWidgetQuickActionOptions,
type WidgetConfig,
widgetsConfig,
} from "@/features/dashboard/widgets/widgets-config";
import { NoteDialog } from "@/features/notes/components/note-dialog";
import { TransactionDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
-import type { SelectOption } from "@/features/transactions/components/types";
import { ExpandableWidgetCard } from "@/shared/components/expandable-widget-card";
import { Button } from "@/shared/components/ui/button";
@@ -47,15 +47,7 @@ type DashboardGridEditableProps = {
data: DashboardData;
period: string;
initialPreferences: WidgetPreferences | null;
- quickActionOptions: {
- payerOptions: SelectOption[];
- splitPayerOptions: SelectOption[];
- defaultPayerId: string | null;
- accountOptions: SelectOption[];
- cardOptions: SelectOption[];
- categoryOptions: SelectOption[];
- estabelecimentos: string[];
- };
+ quickActionOptions: DashboardWidgetQuickActionOptions;
};
const DEFAULT_WIDGET_ORDER = widgetsConfig.map((widget) => widget.id);
@@ -368,11 +360,16 @@ export function DashboardGridEditable({
{widget.component({
data,
period,
+ adminPayerSlug:
+ quickActionOptions.payerOptions.find(
+ (p) => p.value === quickActionOptions.defaultPayerId,
+ )?.slug ?? null,
widgetPreferences: {
order: widgetOrder,
hidden: hiddenWidgets,
myAccountsShowExcluded,
},
+ quickActionOptions,
onMyAccountsShowExcludedChange: setMyAccountsShowExcluded,
})}
diff --git a/src/features/dashboard/components/inbox-widget.tsx b/src/features/dashboard/components/inbox-widget.tsx
new file mode 100644
index 0000000..d3b897a
--- /dev/null
+++ b/src/features/dashboard/components/inbox-widget.tsx
@@ -0,0 +1,267 @@
+"use client";
+
+import {
+ RiCheckboxCircleFill,
+ RiCheckLine,
+ RiDeleteBinLine,
+} from "@remixicon/react";
+import Image from "next/image";
+import { useRouter } from "next/navigation";
+import { useMemo, useState } from "react";
+import { toast } from "sonner";
+import type { DashboardInboxSnapshot } from "@/features/dashboard/inbox-snapshot-queries";
+import type { DashboardWidgetQuickActionOptions } from "@/features/dashboard/widgets/widgets-config";
+import {
+ discardInboxItemAction,
+ markInboxAsProcessedAction,
+} from "@/features/inbox/actions";
+import { TransactionDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
+import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
+import MoneyValues from "@/shared/components/money-values";
+import { Button } from "@/shared/components/ui/button";
+import { WidgetEmptyState } from "@/shared/components/widget-empty-state";
+import { resolveLogoSrc } from "@/shared/lib/logo";
+
+const DEFAULT_INBOX_APP_LOGO = "/avatars/default_icon.png";
+
+function relativeTime(date: Date): string {
+ const diff = Date.now() - date.getTime();
+ const minutes = Math.floor(diff / 60000);
+ if (minutes < 1) return "agora";
+ if (minutes < 60) return `há ${minutes}min`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `há ${hours}h`;
+ const days = Math.floor(hours / 24);
+ return `há ${days}d`;
+}
+
+type InboxWidgetProps = {
+ snapshot: DashboardInboxSnapshot;
+ quickActionOptions: DashboardWidgetQuickActionOptions;
+};
+
+function getDateString(date: Date | string | null | undefined): string | null {
+ if (!date) return null;
+ if (typeof date === "string") return date.slice(0, 10);
+ return date.toISOString().slice(0, 10);
+}
+
+export function InboxWidget({
+ snapshot,
+ quickActionOptions,
+}: InboxWidgetProps) {
+ const router = useRouter();
+ const [processOpen, setProcessOpen] = useState(false);
+ const [discardOpen, setDiscardOpen] = useState(false);
+ const [itemToProcess, setItemToProcess] = useState<
+ DashboardInboxSnapshot["recentItems"][number] | null
+ >(null);
+ const [itemToDiscard, setItemToDiscard] = useState<
+ DashboardInboxSnapshot["recentItems"][number] | null
+ >(null);
+
+ const handleProcessOpenChange = (open: boolean) => {
+ setProcessOpen(open);
+ if (!open) setItemToProcess(null);
+ };
+
+ const handleDiscardOpenChange = (open: boolean) => {
+ setDiscardOpen(open);
+ if (!open) setItemToDiscard(null);
+ };
+
+ const handleProcessRequest = (
+ item: DashboardInboxSnapshot["recentItems"][number],
+ ) => {
+ setItemToProcess(item);
+ setProcessOpen(true);
+ };
+
+ const handleDiscardRequest = (
+ item: DashboardInboxSnapshot["recentItems"][number],
+ ) => {
+ setItemToDiscard(item);
+ setDiscardOpen(true);
+ };
+
+ const refreshWidget = () => {
+ router.refresh();
+ };
+
+ const handleDiscardConfirm = async () => {
+ if (!itemToDiscard) return;
+
+ const result = await discardInboxItemAction({
+ inboxItemId: itemToDiscard.id,
+ });
+
+ if (result.success) {
+ toast.success(result.message);
+ refreshWidget();
+ return;
+ }
+
+ toast.error(result.error);
+ throw new Error(result.error);
+ };
+
+ const handleLancamentoSuccess = async () => {
+ if (!itemToProcess) return;
+
+ const result = await markInboxAsProcessedAction({
+ inboxItemId: itemToProcess.id,
+ });
+
+ if (result.success) {
+ toast.success("Notificação processada!");
+ refreshWidget();
+ return;
+ }
+
+ toast.error(result.error);
+ };
+
+ const defaultPurchaseDate =
+ getDateString(itemToProcess?.notificationTimestamp) ?? 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;
+
+ const matchedCardId = useMemo(() => {
+ const appName = itemToProcess?.sourceAppName?.toLowerCase();
+ if (!appName) return null;
+
+ for (const option of quickActionOptions.cardOptions) {
+ const label = option.label.toLowerCase();
+ if (label.includes(appName) || appName.includes(label)) {
+ return option.value;
+ }
+ }
+
+ return null;
+ }, [itemToProcess?.sourceAppName, quickActionOptions.cardOptions]);
+
+ if (snapshot.pendingCount === 0) {
+ return (
+ }
+ title="Tudo em dia"
+ description="Nenhum pré-lançamento aguardando revisão."
+ />
+ );
+ }
+
+ return (
+
+ {snapshot.recentItems.map((item) => {
+ const displayName = item.parsedName ?? item.originalText.slice(0, 40);
+ const parsedAmount =
+ item.parsedAmount !== null
+ ? Number.parseFloat(item.parsedAmount)
+ : null;
+ const amount =
+ parsedAmount !== null && Number.isFinite(parsedAmount)
+ ? parsedAmount
+ : null;
+ const logoKey = item.sourceAppName?.toLowerCase() ?? "";
+ const rawLogo = snapshot.logoMap[logoKey] ?? null;
+ const logoSrc = resolveLogoSrc(rawLogo);
+ const displayLogo = logoSrc ?? DEFAULT_INBOX_APP_LOGO;
+
+ return (
+
+
+
+
+
+
+ {displayName}
+
+
+ {item.sourceAppName && (
+
+ {item.sourceAppName}
+
+ )}
+
+ {relativeTime(item.createdAt)}
+
+
+
+
+
+
+
+
+ {amount !== null && (
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+ );
+}
diff --git a/src/features/dashboard/fetch-dashboard-data.ts b/src/features/dashboard/fetch-dashboard-data.ts
index 48bb84c..f091634 100644
--- a/src/features/dashboard/fetch-dashboard-data.ts
+++ b/src/features/dashboard/fetch-dashboard-data.ts
@@ -1,7 +1,9 @@
import { cacheLife, cacheTag } from "next/cache";
+import { fetchAttachmentsForPeriod } from "@/features/attachments/queries";
import { fetchDashboardAccounts } from "./accounts-queries";
import { fetchDashboardCategoryOverview } from "./category-overview-queries";
import { fetchDashboardCurrentPeriodOverview } from "./current-period-overview-queries";
+import { fetchDashboardInboxSnapshot } from "./inbox-snapshot-queries";
import { fetchDashboardInvoices } from "./invoices-queries";
import { fetchDashboardNotes } from "./notes-queries";
import { fetchDashboardPayers } from "./payers-queries";
@@ -16,6 +18,8 @@ async function fetchDashboardDataInternal(userId: string, period: string) {
categoryOverview,
pagadoresSnapshot,
notesData,
+ allAttachments,
+ inboxSnapshot,
] = await Promise.all([
fetchDashboardPeriodOverview(userId, period),
fetchDashboardAccounts(userId),
@@ -24,8 +28,27 @@ async function fetchDashboardDataInternal(userId: string, period: string) {
fetchDashboardCategoryOverview(userId, period),
fetchDashboardPayers(userId, period),
fetchDashboardNotes(userId),
+ fetchAttachmentsForPeriod(userId, period),
+ fetchDashboardInboxSnapshot(userId),
]);
+ const attachmentsSnapshot = allAttachments.reduce(
+ (acc, attachment, index) => {
+ acc.totalBytes += attachment.fileSize;
+ if (attachment.mimeType.startsWith("image/")) acc.imageCount++;
+ if (attachment.mimeType === "application/pdf") acc.pdfCount++;
+ if (index < 5) acc.recentAttachments.push(attachment);
+ return acc;
+ },
+ {
+ totalCount: allAttachments.length,
+ totalBytes: 0,
+ imageCount: 0,
+ pdfCount: 0,
+ recentAttachments: [] as typeof allAttachments,
+ },
+ );
+
return {
metrics: periodOverview.metrics,
accountsSnapshot,
@@ -46,6 +69,8 @@ async function fetchDashboardDataInternal(userId: string, period: string) {
purchasesByCategoryData: currentPeriodOverview.purchasesByCategoryData,
incomeByCategoryData: categoryOverview.incomeByCategoryData,
expensesByCategoryData: categoryOverview.expensesByCategoryData,
+ attachmentsSnapshot,
+ inboxSnapshot,
};
}
diff --git a/src/features/dashboard/inbox-snapshot-queries.ts b/src/features/dashboard/inbox-snapshot-queries.ts
new file mode 100644
index 0000000..6bf7a2f
--- /dev/null
+++ b/src/features/dashboard/inbox-snapshot-queries.ts
@@ -0,0 +1,74 @@
+import { and, count, desc, eq } from "drizzle-orm";
+import { cacheLife, cacheTag } from "next/cache";
+import { cards, financialAccounts, inboxItems } from "@/db/schema";
+import { db } from "@/shared/lib/db";
+
+export type DashboardInboxItem = {
+ id: string;
+ sourceAppName: string | null;
+ parsedName: string | null;
+ parsedAmount: string | null;
+ originalText: string;
+ notificationTimestamp: Date;
+ createdAt: Date;
+};
+
+export type DashboardInboxSnapshot = {
+ pendingCount: number;
+ recentItems: DashboardInboxItem[];
+ logoMap: Record;
+};
+
+export async function fetchDashboardInboxSnapshot(
+ userId: string,
+): Promise {
+ "use cache";
+ cacheTag(`dashboard-${userId}`);
+ cacheLife({ revalidate: 3 });
+
+ const [countRows, items, userCards, userAccounts] = await Promise.all([
+ db
+ .select({ total: count() })
+ .from(inboxItems)
+ .where(
+ and(eq(inboxItems.userId, userId), eq(inboxItems.status, "pending")),
+ ),
+ db
+ .select({
+ id: inboxItems.id,
+ sourceAppName: inboxItems.sourceAppName,
+ parsedName: inboxItems.parsedName,
+ parsedAmount: inboxItems.parsedAmount,
+ originalText: inboxItems.originalText,
+ notificationTimestamp: inboxItems.notificationTimestamp,
+ createdAt: inboxItems.createdAt,
+ })
+ .from(inboxItems)
+ .where(
+ and(eq(inboxItems.userId, userId), eq(inboxItems.status, "pending")),
+ )
+ .orderBy(desc(inboxItems.notificationTimestamp))
+ .limit(10),
+ db
+ .select({ name: cards.name, logo: cards.logo })
+ .from(cards)
+ .where(eq(cards.userId, userId)),
+ db
+ .select({ name: financialAccounts.name, logo: financialAccounts.logo })
+ .from(financialAccounts)
+ .where(eq(financialAccounts.userId, userId)),
+ ]);
+
+ const logoMap: Record = {};
+ for (const item of [...userCards, ...userAccounts]) {
+ if (item.logo) {
+ logoMap[item.name.toLowerCase()] = item.logo;
+ }
+ }
+
+ return {
+ pendingCount: Number(countRows[0]?.total ?? 0),
+ recentItems: items,
+ logoMap,
+ };
+}
diff --git a/src/features/dashboard/widgets/widgets-config.tsx b/src/features/dashboard/widgets/widgets-config.tsx
index 5922cdb..f0fc06b 100644
--- a/src/features/dashboard/widgets/widgets-config.tsx
+++ b/src/features/dashboard/widgets/widgets-config.tsx
@@ -1,6 +1,8 @@
import {
RiArrowRightLine,
RiArrowUpDoubleLine,
+ RiAtLine,
+ RiAttachmentLine,
RiBarChartBoxLine,
RiBarcodeLine,
RiBillLine,
@@ -16,9 +18,12 @@ import {
} from "@remixicon/react";
import Link from "next/link";
import type { ReactNode } from "react";
+import { AttachmentsWidget } from "@/features/dashboard/components/attachments-widget";
import { BillWidget } from "@/features/dashboard/components/bill-widget";
+import { CategoryTrendsWidget } from "@/features/dashboard/components/category-trends-widget";
import { ExpensesByCategoryWidgetWithChart } from "@/features/dashboard/components/expenses-by-category-widget-with-chart";
import { GoalsProgressWidget } from "@/features/dashboard/components/goals-progress-widget";
+import { InboxWidget } from "@/features/dashboard/components/inbox-widget";
import { IncomeByCategoryWidgetWithChart } from "@/features/dashboard/components/income-by-category-widget-with-chart";
import { IncomeExpenseBalanceWidget } from "@/features/dashboard/components/income-expense-balance-widget";
import { InstallmentExpensesWidget } from "@/features/dashboard/components/installment-expenses-widget";
@@ -32,8 +37,19 @@ import { PurchasesByCategoryWidget } from "@/features/dashboard/components/purch
import { RecurringExpensesWidget } from "@/features/dashboard/components/recurring-expenses-widget";
import { SpendingOverviewWidget } from "@/features/dashboard/components/spending-overview-widget";
import type { WidgetPreferences } from "@/features/dashboard/widgets/actions";
+import type { SelectOption } from "@/features/transactions/components/types";
import type { DashboardData } from "../fetch-dashboard-data";
+export type DashboardWidgetQuickActionOptions = {
+ payerOptions: SelectOption[];
+ splitPayerOptions: SelectOption[];
+ defaultPayerId: string | null;
+ accountOptions: SelectOption[];
+ cardOptions: SelectOption[];
+ categoryOptions: SelectOption[];
+ estabelecimentos: string[];
+};
+
export type WidgetConfig = {
id: string;
title: string;
@@ -42,7 +58,9 @@ export type WidgetConfig = {
component: (props: {
data: DashboardData;
period: string;
+ adminPayerSlug: string | null;
widgetPreferences: WidgetPreferences;
+ quickActionOptions: DashboardWidgetQuickActionOptions;
onMyAccountsShowExcludedChange?: (value: boolean) => void;
}) => ReactNode;
action?: ReactNode;
@@ -88,21 +106,149 @@ export const widgetsConfig: WidgetConfig[] = [
{
id: "payment-status",
title: "Status de Pagamento",
- subtitle: "Valores Confirmados E Pendentes",
+ subtitle: "Valores confirmados e pendentes",
icon: ,
component: ({ data }) => (
),
},
+ {
+ id: "inbox",
+ title: "Pré-lançamentos",
+ subtitle: "Notificações pendentes de revisão",
+ icon: ,
+ component: ({ data, quickActionOptions }) => (
+
+ ),
+ action: (
+
+ Revisar
+
+
+ ),
+ },
{
id: "income-expense-balance",
title: "Receita, Despesa e Balanço",
- subtitle: "Últimos 6 Meses",
+ subtitle: "Últimos 6 meses",
icon: ,
component: ({ data }) => (
),
},
+ {
+ id: "goals-progress",
+ title: "Progresso de Orçamentos",
+ subtitle: "Orçamentos por categoria no período",
+ icon: ,
+ component: ({ data }) => (
+
+ ),
+ action: (
+
+ Ver todos
+
+
+ ),
+ },
+ {
+ id: "category-trends",
+ title: "Tendências de Categorias",
+ subtitle: "Top 6 maiores variações vs. mês anterior",
+ icon: ,
+ component: ({ data }) => (
+
+ ),
+ },
+ {
+ id: "spending-overview",
+ title: "Panorama de Gastos",
+ subtitle: "Principais despesas e frequência por local",
+ icon: ,
+ component: ({ data }) => (
+
+ ),
+ },
+ {
+ id: "payment-overview",
+ title: "Comportamento de Pagamento",
+ subtitle: "Despesas por condição e forma de pagamento",
+ icon: ,
+ component: ({ data, period, adminPayerSlug }) => (
+
+ ),
+ },
+ {
+ id: "expenses-by-category",
+ title: "Categorias por Despesas",
+ subtitle: "Distribuição de despesas por categoria",
+ icon: ,
+ component: ({ data, period }) => (
+
+ ),
+ },
+ {
+ id: "income-by-category",
+ title: "Categorias por Receitas",
+ subtitle: "Distribuição de receitas por categoria",
+ icon: ,
+ component: ({ data, period }) => (
+
+ ),
+ },
+ {
+ id: "purchases-by-category",
+ title: "Lançamentos por Categorias",
+ subtitle: "Distribuição de lançamentos por categoria",
+ icon: ,
+ component: ({ data }) => (
+
+ ),
+ },
+ {
+ id: "recurring-expenses",
+ title: "Lançamentos Recorrentes",
+ subtitle: "Despesas recorrentes do período",
+ icon: ,
+ component: ({ data }) => (
+
+ ),
+ },
+ {
+ id: "installment-expenses",
+ title: "Lançamentos Parcelados",
+ subtitle: "Acompanhe as parcelas abertas",
+ icon: ,
+ component: ({ data }) => (
+
+ ),
+ },
{
id: "pagadores",
title: "Pagadores",
@@ -138,16 +284,16 @@ export const widgetsConfig: WidgetConfig[] = [
),
},
{
- id: "goals-progress",
- title: "Progresso de Orçamentos",
- subtitle: "Orçamentos por categoria no período",
- icon: ,
+ id: "attachments",
+ title: "Anexos",
+ subtitle: "Comprovantes do período",
+ icon: ,
component: ({ data }) => (
-
+
),
action: (
Ver todos
@@ -155,80 +301,4 @@ export const widgetsConfig: WidgetConfig[] = [
),
},
- {
- id: "payment-overview",
- title: "Comportamento de Pagamento",
- subtitle: "Despesas por condição e forma de pagamento",
- icon: ,
- component: ({ data }) => (
-
- ),
- },
- {
- id: "recurring-expenses",
- title: "Lançamentos Recorrentes",
- subtitle: "Despesas recorrentes do período",
- icon: ,
- component: ({ data }) => (
-
- ),
- },
- {
- id: "installment-expenses",
- title: "Lançamentos Parcelados",
- subtitle: "Acompanhe as parcelas abertas",
- icon: ,
- component: ({ data }) => (
-
- ),
- },
- {
- id: "spending-overview",
- title: "Panorama de Gastos",
- subtitle: "Principais despesas e frequência por local",
- icon: ,
- component: ({ data }) => (
-
- ),
- },
- {
- id: "purchases-by-category",
- title: "Lançamentos por Categorias",
- subtitle: "Distribuição de lançamentos por categoria",
- icon: ,
- component: ({ data }) => (
-
- ),
- },
- {
- id: "income-by-category",
- title: "Categorias por Receitas",
- subtitle: "Distribuição de receitas por categoria",
- icon: ,
- component: ({ data, period }) => (
-
- ),
- },
- {
- id: "expenses-by-category",
- title: "Categorias por Despesas",
- subtitle: "Distribuição de despesas por categoria",
- icon: ,
- component: ({ data, period }) => (
-
- ),
- },
];
diff --git a/src/proxy.ts b/src/proxy.ts
index 011a964..0fe5f17 100644
--- a/src/proxy.ts
+++ b/src/proxy.ts
@@ -49,6 +49,7 @@ function buildCsp(): string {
`img-src 'self' ${imgExtras} data: blob:`,
"font-src 'self'",
`connect-src 'self' ${connectExtras}`,
+ `frame-src 'self'${s3Origin ? ` ${s3Origin}` : ""}`,
"frame-ancestors 'none'",
].join("; ");
}