@@ -265,72 +252,34 @@ export default async function Page() {
-
- {mainFeatures.map((feature) => (
+
+ {[...mainFeatures, ...extraFeatures].map((feature) => (
-
-
+
+
-
-
- {feature.title}
-
-
- {feature.description}
-
-
+
+ {feature.title}
+
+
+ {feature.description}
+
))}
-
-
-
-
- Também inclui
-
-
- {extraFeatures.map((feature) => (
-
-
-
-
-
-
- {feature.title}
-
-
- {feature.description}
-
-
-
- ))}
-
-
-
@@ -396,14 +345,14 @@ export default async function Page() {
{pwaHighlights.map((item) => (
@@ -438,17 +387,19 @@ export default async function Page() {
pré-lançamentos automaticamente para você revisar na inbox.
- {companionSteps.map((step, index) => (
+ {companionSteps.map((step) => (
-
-
- {index + 1}
-
+
+
{step.title}
@@ -545,14 +496,14 @@ export default async function Page() {
@@ -633,14 +584,14 @@ export default async function Page() {
diff --git a/src/app/globals.css b/src/app/globals.css
index 4724d60..282146f 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -10,7 +10,7 @@
:root {
--background: oklch(97.412% 0.00332 67.032);
--foreground: oklch(27% 0.008 45);
- --card: oklch(99% 0.002 67);
+ --card: oklch(100% 0 0);
--card-foreground: var(--foreground);
--popover: oklch(100% 0 0);
--popover-foreground: var(--foreground);
@@ -36,7 +36,7 @@
--destructive: oklch(55% 0.22 27);
--destructive-foreground: oklch(98% 0.005 30);
- --border: oklch(90.274% 0.01362 60.342);
+ --border: oklch(92.323% 0.01276 63.703);
--input: var(--border);
--ring: var(--primary);
@@ -57,10 +57,6 @@
--data-4: oklch(74% 0.18 55); /* âmbar */
--data-5: oklch(78% 0.16 68); /* âmbar-dourado */
--data-6: oklch(76% 0.15 82); /* amarelo-quente */
- --data-7: oklch(70% 0.17 95); /* amarelo-lima */
- --data-8: oklch(65% 0.18 108); /* lima-verde */
- --data-9: oklch(62% 0.17 120); /* verde-oliva claro */
- --data-10: oklch(56% 0.15 10); /* terracota escuro */
--sidebar: oklch(99.3% 0.0015 75);
--sidebar-foreground: var(--foreground);
@@ -71,7 +67,7 @@
--sidebar-border: oklch(91% 0.004 70);
--sidebar-ring: var(--primary);
- --radius: 0.625rem;
+ --radius: 0.7rem;
--shadow-2xs: 0 1px 2px 0px oklch(35% 0.02 45 / 0.04);
--shadow-xs: 0 1px 3px 0px oklch(35% 0.02 45 / 0.06);
@@ -94,7 +90,7 @@
.dark {
--background: oklch(18% 0.004 55);
--foreground: oklch(93% 0.008 80);
- --card: oklch(21.5% 0.004 55);
+ --card: oklch(21.531% 0.00369 48.293);
--card-foreground: var(--foreground);
--popover: oklch(24% 0.004 55);
--popover-foreground: var(--foreground);
@@ -120,7 +116,7 @@
--destructive: oklch(62% 0.2 28);
--destructive-foreground: oklch(98% 0.005 30);
- --border: oklch(31% 0.004 55);
+ --border: oklch(28% 0.0035 55);
--input: var(--border);
--ring: var(--primary);
@@ -141,10 +137,6 @@
--data-4: oklch(81% 0.18 55);
--data-5: oklch(84% 0.16 68);
--data-6: oklch(82% 0.15 82);
- --data-7: oklch(77% 0.17 95);
- --data-8: oklch(72% 0.18 108);
- --data-9: oklch(69% 0.17 120);
- --data-10: oklch(63% 0.15 10);
--sidebar: oklch(15.5% 0.004 55);
--sidebar-foreground: var(--foreground);
@@ -155,7 +147,7 @@
--sidebar-border: oklch(30% 0.004 55);
--sidebar-ring: var(--primary);
- --radius: 0.625rem;
+ --radius: 0.7rem;
--shadow-2xs: 0 1px 2px 0px oklch(0% 0 0 / 0.3);
--shadow-xs: 0 1px 3px 0px oklch(0% 0 0 / 0.4);
diff --git a/src/features/accounts/components/account-card.tsx b/src/features/accounts/components/account-card.tsx
index d323159..26af83b 100644
--- a/src/features/accounts/components/account-card.tsx
+++ b/src/features/accounts/components/account-card.tsx
@@ -1,4 +1,5 @@
"use client";
+
import {
RiArrowLeftRightLine,
RiDeleteBin5Line,
@@ -47,6 +48,13 @@ export function AccountCard({
}: AccountCardProps) {
const isInactive = status?.toLowerCase() === "inativa";
+ const balanceColor =
+ balance > 0
+ ? "text-success"
+ : balance < 0
+ ? "text-destructive"
+ : "text-foreground";
+
const actions = [
{
label: "editar",
@@ -75,78 +83,90 @@ export function AccountCard({
].filter((action) => typeof action.onClick === "function");
return (
-
-
-
- {icon ? (
-
- {icon}
+
+
+
+
+ {icon}
+
+
+
+
+ {accountName}
+
+ {excludeFromBalance || excludeInitialBalanceFromIncome ? (
+
+
+
+
+
+
+ {excludeFromBalance && (
+
+ Desconsiderado do saldo total: Esta
+ conta não é incluída no cálculo do saldo total geral.
+
+ )}
+ {excludeInitialBalanceFromIncome && (
+
+
+ Saldo inicial desconsiderado das receitas:
+ {" "}
+ O saldo inicial desta conta não é contabilizado como
+ receita nas métricas.
+
+ )}
+
+
+
+ ) : null}
- ) : null}
-
- {accountName}
-
- {(excludeFromBalance || excludeInitialBalanceFromIncome) && (
-
-
-
-
-
-
-
-
- {excludeFromBalance && (
-
- Desconsiderado do saldo total: Esta conta
- não é incluída no cálculo do saldo total geral.
-
- )}
- {excludeInitialBalanceFromIncome && (
-
-
- Saldo inicial desconsiderado das receitas:
- {" "}
- O saldo inicial desta conta não é contabilizado como
- receita nas métricas.
-
- )}
-
-
-
- )}
+
{status}
+
-
-
-
{accountType}
+
{accountType}
+
+
+
+
+ Saldo
+
- {actions.length > 0 ? (
-
- {actions.map(({ label, icon, onClick, variant }) => (
-
- ))}
-
- ) : null}
+
+ {actions.map(({ label, icon, onClick, variant }) => (
+
+ ))}
+
);
}
diff --git a/src/features/accounts/components/account-statement-card.tsx b/src/features/accounts/components/account-statement-card.tsx
index 750a8db..fe38f53 100644
--- a/src/features/accounts/components/account-statement-card.tsx
+++ b/src/features/accounts/components/account-statement-card.tsx
@@ -86,7 +86,7 @@ export function AccountStatementCard({
-
-
-
-
- {children}
-
+
+
+ {children}
diff --git a/src/features/budgets/components/budget-card.tsx b/src/features/budgets/components/budget-card.tsx
index be51b42..2e56ad4 100644
--- a/src/features/budgets/components/budget-card.tsx
+++ b/src/features/budgets/components/budget-card.tsx
@@ -15,7 +15,6 @@ import type { Budget } from "./types";
interface BudgetCardProps {
budget: Budget;
- periodLabel: string;
onEdit: (budget: Budget) => void;
onRemove: (budget: Budget) => void;
}
@@ -29,81 +28,88 @@ const buildUsagePercent = (spent: number, limit: number) => {
};
const formatCategoryName = (budget: Budget) =>
- budget.category?.name ?? "Category removida";
+ budget.category?.name ?? "Categoria removida";
-export function BudgetCard({
- budget,
- periodLabel,
- onEdit,
- onRemove,
-}: BudgetCardProps) {
+export function BudgetCard({ budget, onEdit, onRemove }: BudgetCardProps) {
const { amount: limit, spent } = budget;
const exceeded = spent > limit && limit >= 0;
const difference = Math.abs(spent - limit);
const usagePercent = buildUsagePercent(spent, limit);
+ const remaining = Math.max(limit - spent, 0);
return (
-
-
-
-
+
+
+
+
+ {formatCategoryName(budget)}
+
+
+
+
+
+
+
+ {exceeded ? "Excedido em" : "Disponível"}
+
+
-
-
- {formatCategoryName(budget)}
-
-
- Orçamento de {periodLabel}
-
-
-
-
-
Gasto até agora
+
+
+ Orçamento
-
-
- Limite
-
-
-
-
- {exceeded ? (
-
- Excedeu em
-
- ) : (
-
- Restam {" "}
- disponíveis.
-
- )}
+
+ Gasto
+
+
+
+
+
+ {usagePercent.toFixed(1)}% utilizado
+
+
-
+
+
{budget.category && (
detalhes
@@ -111,7 +117,7 @@ export function BudgetCard({
diff --git a/src/features/budgets/components/budgets-page.tsx b/src/features/budgets/components/budgets-page.tsx
index e5d45fd..5c4f572 100644
--- a/src/features/budgets/components/budgets-page.tsx
+++ b/src/features/budgets/components/budgets-page.tsx
@@ -19,14 +19,12 @@ interface BudgetsPageProps {
budgets: Budget[];
categories: BudgetCategory[];
selectedPeriod: string;
- periodLabel: string;
}
export function BudgetsPage({
budgets,
categories,
selectedPeriod,
- periodLabel,
}: BudgetsPageProps) {
const [editOpen, setEditOpen] = useState(false);
const [selectedBudget, setSelectedBudget] = useState(null);
@@ -137,7 +135,6 @@ export function BudgetsPage({
diff --git a/src/features/budgets/queries.ts b/src/features/budgets/queries.ts
index b350018..2d3e902 100644
--- a/src/features/budgets/queries.ts
+++ b/src/features/budgets/queries.ts
@@ -13,7 +13,7 @@ const toNumber = (value: string | number | null | undefined) => {
return 0;
};
-export type BudgetData = {
+type BudgetData = {
id: string;
amount: number;
spent: number;
diff --git a/src/features/calendar/components/calendar-grid.tsx b/src/features/calendar/components/calendar-grid.tsx
index 638510f..ede1eee 100644
--- a/src/features/calendar/components/calendar-grid.tsx
+++ b/src/features/calendar/components/calendar-grid.tsx
@@ -1,10 +1,8 @@
"use client";
import { DayCell } from "@/features/calendar/components/day-cell";
-
import type { CalendarDay } from "@/shared/lib/types/calendar";
import { WEEK_DAYS_SHORT } from "@/shared/utils/calendar";
-import { cn } from "@/shared/utils/ui";
type CalendarGridProps = {
days: CalendarDay[];
@@ -18,21 +16,18 @@ export function CalendarGrid({
onCreateDay,
}: CalendarGridProps) {
return (
-
+
{WEEK_DAYS_SHORT.map((dayName) => (
-
+
{dayName}
))}
-
+
{days.map((day) => (
-
+
))}
diff --git a/src/features/calendar/components/calendar-legend.tsx b/src/features/calendar/components/calendar-legend.tsx
index dcc763d..81f46eb 100644
--- a/src/features/calendar/components/calendar-legend.tsx
+++ b/src/features/calendar/components/calendar-legend.tsx
@@ -1,34 +1,32 @@
"use client";
import { EVENT_TYPE_STYLES } from "@/features/calendar/components/day-cell";
-import StatusDot from "@/shared/components/status-dot";
-import { Card } from "@/shared/components/ui/card";
-import type { CalendarEvent } from "@/shared/lib/types/calendar";
+import { cn } from "@/shared/utils/ui";
-const LEGEND_ITEMS: Array<{
- type?: CalendarEvent["type"];
- label: string;
- dotColor?: string;
-}> = [
- { type: "transaction", label: "Lançamentos" },
- { type: "boleto", label: "Boleto com vencimento" },
- { type: "card", label: "Vencimento de cartão" },
- { label: "Pagamento fatura", dotColor: "bg-success" },
+const LEGEND_ITEMS = [
+ { label: "Lançamentos", ...EVENT_TYPE_STYLES.transaction },
+ { label: "Boletos", ...EVENT_TYPE_STYLES.boleto },
+ { label: "Fatura de Cartão", ...EVENT_TYPE_STYLES.card },
];
export function CalendarLegend() {
return (
-
- {LEGEND_ITEMS.map((item, index) => {
- const dotColor =
- item.dotColor || (item.type ? EVENT_TYPE_STYLES[item.type].dot : "");
- return (
-
-
- {item.label}
-
- );
- })}
-
+
+ {LEGEND_ITEMS.map((item) => (
+ -
+
+ {item.label}
+
+ ))}
+
);
}
diff --git a/src/features/calendar/components/day-cell.tsx b/src/features/calendar/components/day-cell.tsx
index eae9b3f..0e3f386 100644
--- a/src/features/calendar/components/day-cell.tsx
+++ b/src/features/calendar/components/day-cell.tsx
@@ -1,6 +1,6 @@
"use client";
-import { RiAddLine } from "@remixicon/react";
+import { RiAddLine, RiCheckboxCircleFill } from "@remixicon/react";
import type { KeyboardEvent, MouseEvent } from "react";
import type { CalendarDay, CalendarEvent } from "@/shared/lib/types/calendar";
import { currencyFormatter } from "@/shared/utils/currency";
@@ -14,44 +14,33 @@ type DayCellProps = {
export const EVENT_TYPE_STYLES: Record<
CalendarEvent["type"],
- { wrapper: string; dot: string; accent?: string }
+ { wrapper: string; dot: string }
> = {
transaction: {
- wrapper:
- "bg-warning/10 text-warning dark:bg-warning/5 dark:text-warning border-l-4 border-warning",
- dot: "bg-warning",
+ wrapper: "bg-primary/10 text-primary dark:bg-primary/5 dark:text-primary",
+ dot: "bg-primary",
},
boleto: {
- wrapper:
- "bg-info/10 text-info dark:bg-info/5 dark:text-info border-l-4 border-info",
+ wrapper: "bg-info/10 text-info dark:bg-info/5 dark:text-info",
dot: "bg-info",
},
card: {
wrapper:
- "bg-violet-100 text-violet-600 dark:bg-violet-900/10 dark:text-violet-50 border-l-4 border-violet-500",
- dot: "bg-violet-600",
+ "bg-violet-100 text-violet-600 dark:bg-violet-900/10 dark:text-violet-500",
+ dot: "bg-violet-600 dark:bg-violet-500",
},
};
-const eventStyles = EVENT_TYPE_STYLES;
-
const formatCurrencyValue = (value: number | null | undefined) =>
currencyFormatter.format(Math.abs(value ?? 0));
-const formatAmount = (event: Extract
) =>
- formatCurrencyValue(event.transaction.amount);
-
const buildEventLabel = (event: CalendarEvent) => {
switch (event.type) {
- case "transaction": {
+ case "transaction":
+ case "boleto":
return event.transaction.name;
- }
- case "boleto": {
- return event.transaction.name;
- }
- case "card": {
+ case "card":
return event.card.name;
- }
default:
return "";
}
@@ -59,60 +48,48 @@ const buildEventLabel = (event: CalendarEvent) => {
const buildEventComplement = (event: CalendarEvent) => {
switch (event.type) {
- case "transaction": {
- return formatAmount(event);
- }
- case "boleto": {
+ case "transaction":
+ case "boleto":
return formatCurrencyValue(event.transaction.amount);
- }
- case "card": {
- if (event.card.totalDue !== null) {
- return formatCurrencyValue(event.card.totalDue);
- }
- return null;
- }
+ case "card":
+ return event.card.totalDue !== null
+ ? formatCurrencyValue(event.card.totalDue)
+ : null;
default:
return null;
}
};
-const isPagamentoFatura = (event: CalendarEvent) => {
- return (
- event.type === "transaction" &&
- event.transaction.name.startsWith("Pagamento fatura -")
- );
-};
-
-const getEventStyle = (event: CalendarEvent) => {
- if (isPagamentoFatura(event)) {
- return {
- wrapper:
- "bg-success/10 text-success dark:bg-success/5 dark:text-success border-l-4 border-success",
- dot: "bg-success",
- };
- }
- return eventStyles[event.type];
+const isPaid = (event: CalendarEvent) => {
+ if (event.type === "boleto") return Boolean(event.transaction.isSettled);
+ if (event.type === "card") return event.card.isPaid;
+ return false;
};
const DayEventPreview = ({ event }: { event: CalendarEvent }) => {
const complement = buildEventComplement(event);
const label = buildEventLabel(event);
- const style = getEventStyle(event);
+ const style = EVENT_TYPE_STYLES[event.type];
return (
+
{label}
+ {isPaid(event) && (
+
+ )}
{complement ? (
-
- {complement}
-
+
{complement}
) : null}
);
@@ -143,8 +120,8 @@ export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
onClick={() => onSelect(day)}
onKeyDown={handleKeyDown}
className={cn(
- "group flex h-full cursor-pointer flex-col gap-1.5 rounded-lg border border-transparent bg-card/80 p-2 text-left transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:border-primary/40 hover:bg-primary/5 dark:hover:bg-accent",
- !day.isCurrentMonth && "opacity-60",
+ "group flex h-full cursor-pointer flex-col gap-1.5 rounded-lg border bg-card/80 p-2 text-left transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:border-primary/40 hover:bg-primary/5 dark:hover:bg-accent",
+ !day.isCurrentMonth && "bg-muted/20 opacity-60",
day.isToday && "border-primary/70 bg-primary/5 hover:border-primary",
)}
>
@@ -159,14 +136,16 @@ export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
>
{day.label}
-
+ {day.isCurrentMonth && (
+
+ )}
diff --git a/src/features/calendar/components/event-modal.tsx b/src/features/calendar/components/event-modal.tsx
index e44657f..ae9229d 100644
--- a/src/features/calendar/components/event-modal.tsx
+++ b/src/features/calendar/components/event-modal.tsx
@@ -1,5 +1,6 @@
"use client";
+import { RiCalendarEventLine } from "@remixicon/react";
import type { ReactNode } from "react";
import { EVENT_TYPE_STYLES } from "@/features/calendar/components/day-cell";
import MoneyValues from "@/shared/components/money-values";
@@ -29,17 +30,13 @@ type EventModalProps = {
const EventCard = ({
children,
type,
- isPagamentoFatura = false,
}: {
children: ReactNode;
type: CalendarEvent["type"];
- isPagamentoFatura?: boolean;
}) => {
- const style = isPagamentoFatura
- ? { dot: "bg-success" }
- : EVENT_TYPE_STYLES[type];
+ const style = EVENT_TYPE_STYLES[type];
return (
-
+
,
) => {
const isReceita = event.transaction.transactionType === "Receita";
- const isPagamentoFatura =
- event.transaction.name.startsWith("Pagamento fatura -");
return (
-
+
-
+
{event.transaction.name}
-
-
- {event.transaction.categoriaName}
-
+ {event.transaction.categoriaName}
-
-
-
+ amount={event.transaction.amount}
+ />
);
@@ -91,59 +81,80 @@ const renderLancamento = (
const renderBoleto = (event: Extract) => {
const isPaid = Boolean(event.transaction.isSettled);
- const dueDate = event.transaction.dueDate;
- const dueDateLabel = formatFinancialDateLabel(dueDate, "Vence em", {
- day: "2-digit",
- month: "2-digit",
- year: "numeric",
- });
+ const dueDateLabel = formatFinancialDateLabel(
+ event.transaction.dueDate,
+ "Vence em",
+ DATE_FORMAT,
+ );
+ const paymentDateLabel = isPaid
+ ? formatFinancialDateLabel(
+ event.transaction.boletoPaymentDate,
+ "Pago em",
+ DATE_FORMAT,
+ )
+ : null;
return (
-
-
- {event.transaction.name}
-
-
+
+ {event.transaction.name}
+
+
{dueDateLabel && (
-
- {dueDateLabel}
-
+ {dueDateLabel}
+ )}
+ {paymentDateLabel && (
+ {paymentDateLabel}
)}
-
-
{isPaid ? "Pago" : "Pendente"}
+
{isPaid ? "Pago" : "Pendente"}
-
-
-
+
);
};
-const renderCard = (event: Extract
) => (
-
-
-
-
-
- Vencimento Fatura - {event.card.name}
-
-
+const renderCard = (event: Extract
) => {
+ const paymentDateLabel = event.card.isPaid
+ ? formatFinancialDateLabel(event.card.paymentDate, "Pago em", DATE_FORMAT)
+ : null;
- {event.card.status ?? "Invoice"}
+ return (
+
+
+
+
+ Vencimento Fatura — {event.card.name}
+
+ {paymentDateLabel && (
+ {paymentDateLabel}
+ )}
+
+ {event.card.isPaid ? "Pago" : (event.card.status ?? "Fatura")}
+
+
+ {event.card.totalDue !== null ? (
+
+ ) : null}
- {event.card.totalDue !== null ? (
-
-
-
- ) : null}
-
-
-);
+
+ );
+};
+
+const SECTION_LABELS: Record
= {
+ transaction: "Lançamentos",
+ boleto: "Boletos",
+ card: "Faturas",
+};
const renderEvent = (event: CalendarEvent) => {
switch (event.type) {
@@ -169,28 +180,50 @@ export function EventModal({ open, day, onClose, onCreate }: EventModalProps) {
onCreate(day.date);
};
- const description = day?.events.length
- ? "Confira os lançamentos e vencimentos cadastrados para este dia."
- : "Nenhum lançamento encontrado para este dia. Você pode criar um novo lançamento agora.";
+ const hasEvents = Boolean(day?.events.length);
+
+ const grouped = day
+ ? {
+ transaction: day.events.filter((e) => e.type === "transaction"),
+ boleto: day.events.filter((e) => e.type === "boleto"),
+ card: day.events.filter((e) => e.type === "card"),
+ }
+ : null;
return (