feat(reports): melhora notas, calendario e analises

This commit is contained in:
Felipe Coutinho
2026-03-09 17:14:04 +00:00
parent ada1377640
commit 6205dee42a
35 changed files with 429 additions and 590 deletions

View File

@@ -2,8 +2,8 @@
import { DayCell } from "@/components/calendario/day-cell";
import type { CalendarDay } from "@/components/calendario/types";
import { WEEK_DAYS_SHORT } from "@/components/calendario/utils";
import type { CalendarDay } from "@/lib/types/calendario";
import { WEEK_DAYS_SHORT } from "@/lib/utils/calendario";
import { cn } from "@/lib/utils/ui";
type CalendarGridProps = {
@@ -18,10 +18,10 @@ export function CalendarGrid({
onCreateDay,
}: CalendarGridProps) {
return (
<div className="overflow-hidden rounded-lg bg-card drop-shadow-xs px-2">
<div className="overflow-hidden rounded-lg bg-card drop-shadow-xs border-none">
<div className="grid grid-cols-7 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
{WEEK_DAYS_SHORT.map((dayName) => (
<span key={dayName} className="px-3 py-2 text-center">
<span key={dayName} className="px-3 py-2 text-center text-primary">
{dayName}
</span>
))}

View File

@@ -1,8 +1,9 @@
"use client";
import { EVENT_TYPE_STYLES } from "@/components/calendario/day-cell";
import type { CalendarEvent } from "@/components/calendario/types";
import { cn } from "@/lib/utils/ui";
import type { CalendarEvent } from "@/lib/types/calendario";
import StatusDot from "../shared/status-dot";
import { Card } from "../ui/card";
const LEGEND_ITEMS: Array<{
type?: CalendarEvent["type"];
@@ -17,17 +18,17 @@ const LEGEND_ITEMS: Array<{
export function CalendarLegend() {
return (
<div className="flex flex-wrap gap-3 rounded-sm border border-border/60 bg-muted/20 p-2 text-xs font-medium text-muted-foreground">
<Card className="flex flex-row gap-2 p-2 text-sm">
{LEGEND_ITEMS.map((item, index) => {
const dotColor =
item.dotColor || (item.type ? EVENT_TYPE_STYLES[item.type].dot : "");
return (
<span key={item.type || index} className="flex items-center gap-2">
<span className={cn("size-3 rounded-full", dotColor)} />
<StatusDot color={dotColor} />
{item.label}
</span>
);
})}
</div>
</Card>
);
}

View File

@@ -2,7 +2,7 @@
import { RiAddLine } from "@remixicon/react";
import type { KeyboardEvent, MouseEvent } from "react";
import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
import type { CalendarDay, CalendarEvent } from "@/lib/types/calendario";
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
import { cn } from "@/lib/utils/ui";

View File

@@ -2,7 +2,7 @@
import type { ReactNode } from "react";
import { EVENT_TYPE_STYLES } from "@/components/calendario/day-cell";
import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
import MoneyValues from "@/components/shared/money-values";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -12,9 +12,10 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import type { CalendarDay, CalendarEvent } from "@/lib/types/calendario";
import { friendlyDate, parseLocalDateString } from "@/lib/utils/date";
import { formatFinancialDateLabel } from "@/lib/utils/financial-dates";
import { cn } from "@/lib/utils/ui";
import MoneyValues from "../money-values";
import { Badge } from "../ui/badge";
import { Card } from "../ui/card";
@@ -93,9 +94,11 @@ const renderLancamento = (
const renderBoleto = (event: Extract<CalendarEvent, { type: "boleto" }>) => {
const isPaid = Boolean(event.lancamento.isSettled);
const dueDate = event.lancamento.dueDate;
const formattedDueDate = dueDate
? new Intl.DateTimeFormat("pt-BR").format(new Date(dueDate))
: null;
const dueDateLabel = formatFinancialDateLabel(dueDate, "Vence em", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
return (
<EventCard type="boleto">
@@ -106,9 +109,9 @@ const renderBoleto = (event: Extract<CalendarEvent, { type: "boleto" }>) => {
{event.lancamento.name}
</span>
{formattedDueDate && (
{dueDateLabel && (
<span className="text-xs text-muted-foreground leading-tight">
Vence em {formattedDueDate}
{dueDateLabel}
</span>
)}
</div>

View File

@@ -1,17 +1,18 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { CalendarGrid } from "@/components/calendario/calendar-grid";
import { CalendarLegend } from "@/components/calendario/calendar-legend";
import { EventModal } from "@/components/calendario/event-modal";
import { LancamentoDialog } from "@/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog";
import type {
CalendarDay,
CalendarEvent,
CalendarFormOptions,
CalendarPeriod,
} from "@/components/calendario/types";
import { buildCalendarDays } from "@/components/calendario/utils";
import { LancamentoDialog } from "@/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog";
} from "@/lib/types/calendario";
import { buildCalendarDays } from "@/lib/utils/calendario";
import { parsePeriod } from "@/lib/utils/period";
type MonthlyCalendarProps = {
period: CalendarPeriod;
@@ -19,23 +20,13 @@ type MonthlyCalendarProps = {
formOptions: CalendarFormOptions;
};
const parsePeriod = (period: string) => {
const [yearStr, monthStr] = period.split("-");
const year = Number.parseInt(yearStr ?? "", 10);
const month = Number.parseInt(monthStr ?? "", 10);
return { year, monthIndex: month - 1 };
};
export function MonthlyCalendar({
period,
events,
formOptions,
}: MonthlyCalendarProps) {
const { year, monthIndex } = useMemo(
() => parsePeriod(period.period),
[period.period],
);
const { year, month } = parsePeriod(period.period);
const monthIndex = month - 1;
const eventsByDay = useMemo(() => {
const map = new Map<string, CalendarEvent[]>();
@@ -57,35 +48,32 @@ export function MonthlyCalendar({
const [createOpen, setCreateOpen] = useState(false);
const [createDate, setCreateDate] = useState<string | null>(null);
const handleOpenCreate = useCallback((date: string) => {
const handleOpenCreate = (date: string) => {
setCreateDate(date);
setModalOpen(false);
setCreateOpen(true);
}, []);
};
const handleDaySelect = useCallback((day: CalendarDay) => {
const handleDaySelect = (day: CalendarDay) => {
setSelectedDay(day);
setModalOpen(true);
}, []);
};
const handleCreateFromCell = useCallback(
(day: CalendarDay) => {
handleOpenCreate(day.date);
},
[handleOpenCreate],
);
const handleCreateFromCell = (day: CalendarDay) => {
handleOpenCreate(day.date);
};
const handleModalClose = useCallback(() => {
const handleModalClose = () => {
setModalOpen(false);
setSelectedDay(null);
}, []);
};
const handleCreateDialogChange = useCallback((open: boolean) => {
const handleCreateDialogChange = (open: boolean) => {
setCreateOpen(open);
if (!open) {
setCreateDate(null);
}
}, []);
};
return (
<>

View File

@@ -1,62 +0,0 @@
import type {
LancamentoItem,
SelectOption,
} from "@/components/lancamentos/types";
export type CalendarEvent =
| {
id: string;
type: "lancamento";
date: string;
lancamento: LancamentoItem;
}
| {
id: string;
type: "boleto";
date: string;
lancamento: LancamentoItem;
}
| {
id: string;
type: "cartao";
date: string;
card: {
id: string;
name: string;
dueDay: string;
closingDay: string;
brand: string | null;
status: string;
logo: string | null;
totalDue: number | null;
};
};
export type CalendarPeriod = {
period: string;
monthName: string;
year: number;
};
export type CalendarDay = {
date: string;
label: string;
isCurrentMonth: boolean;
isToday: boolean;
events: CalendarEvent[];
};
export type CalendarFormOptions = {
pagadorOptions: SelectOption[];
splitPagadorOptions: SelectOption[];
defaultPagadorId: string | null;
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
estabelecimentos: string[];
};
export type CalendarData = {
events: CalendarEvent[];
formOptions: CalendarFormOptions;
};

View File

@@ -1,60 +0,0 @@
import type { CalendarDay, CalendarEvent } from "@/components/calendario/types";
export const formatDateKey = (date: Date) => date.toISOString().slice(0, 10);
const getWeekdayIndex = (date: Date) => {
const day = date.getUTCDay(); // 0 (domingo) - 6 (sábado)
// Ajusta para segunda-feira como primeiro dia
return day === 0 ? 6 : day - 1;
};
export const buildCalendarDays = ({
year,
monthIndex,
events,
}: {
year: number;
monthIndex: number;
events: Map<string, CalendarEvent[]>;
}): CalendarDay[] => {
const startOfMonth = new Date(Date.UTC(year, monthIndex, 1));
const offset = getWeekdayIndex(startOfMonth);
const startDate = new Date(Date.UTC(year, monthIndex, 1 - offset));
const totalCells = 42; // 6 semanas
const now = new Date();
const todayKey = formatDateKey(
new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())),
);
const days: CalendarDay[] = [];
for (let index = 0; index < totalCells; index += 1) {
const currentDate = new Date(startDate);
currentDate.setUTCDate(startDate.getUTCDate() + index);
const dateKey = formatDateKey(currentDate);
const isCurrentMonth = currentDate.getUTCMonth() === monthIndex;
const dateLabel = currentDate.getUTCDate().toString();
const eventsForDay = events.get(dateKey) ?? [];
days.push({
date: dateKey,
label: dateLabel,
isCurrentMonth,
isToday: dateKey === todayKey,
events: eventsForDay,
});
}
return days;
};
export const WEEK_DAYS_SHORT = [
"Seg",
"Ter",
"Qua",
"Qui",
"Sex",
"Sáb",
"Dom",
];