diff --git a/src/features/calendar/components/calendar-legend.tsx b/src/features/calendar/components/calendar-legend.tsx index 81f46eb..00e75c2 100644 --- a/src/features/calendar/components/calendar-legend.tsx +++ b/src/features/calendar/components/calendar-legend.tsx @@ -5,6 +5,7 @@ import { cn } from "@/shared/utils/ui"; const LEGEND_ITEMS = [ { label: "Lançamentos", ...EVENT_TYPE_STYLES.transaction }, + { label: "Parcelas", ...EVENT_TYPE_STYLES.installment }, { label: "Boletos", ...EVENT_TYPE_STYLES.boleto }, { label: "Fatura de Cartão", ...EVENT_TYPE_STYLES.card }, ]; diff --git a/src/features/calendar/components/day-cell.tsx b/src/features/calendar/components/day-cell.tsx index 0e3f386..84ce7d1 100644 --- a/src/features/calendar/components/day-cell.tsx +++ b/src/features/calendar/components/day-cell.tsx @@ -20,6 +20,11 @@ export const EVENT_TYPE_STYLES: Record< wrapper: "bg-primary/10 text-primary dark:bg-primary/5 dark:text-primary", dot: "bg-primary", }, + installment: { + wrapper: + "bg-amber-100 text-amber-600 dark:bg-amber-900/10 dark:text-amber-500", + dot: "bg-amber-500", + }, boleto: { wrapper: "bg-info/10 text-info dark:bg-info/5 dark:text-info", dot: "bg-info", @@ -39,6 +44,8 @@ const buildEventLabel = (event: CalendarEvent) => { case "transaction": case "boleto": return event.transaction.name; + case "installment": + return event.transaction.name; case "card": return event.card.name; default: @@ -51,6 +58,8 @@ const buildEventComplement = (event: CalendarEvent) => { case "transaction": case "boleto": return formatCurrencyValue(event.transaction.amount); + case "installment": + return `${event.installmentCount}x de ${formatCurrencyValue(event.installmentValue)}`; case "card": return event.card.totalDue !== null ? formatCurrencyValue(event.card.totalDue) diff --git a/src/features/calendar/components/event-modal.tsx b/src/features/calendar/components/event-modal.tsx index ae9229d..b42dfa9 100644 --- a/src/features/calendar/components/event-modal.tsx +++ b/src/features/calendar/components/event-modal.tsx @@ -150,8 +150,39 @@ const renderCard = (event: Extract) => { ); }; +const renderInstallment = ( + event: Extract, +) => { + const isReceita = event.transaction.transactionType === "Receita"; + + return ( + +
+
+ + {event.transaction.name} + + {event.installmentCount}x parcelas +
+
+ + por parcela +
+
+
+ ); +}; + const SECTION_LABELS: Record = { transaction: "Lançamentos", + installment: "Parcelas", boleto: "Boletos", card: "Faturas", }; @@ -160,6 +191,8 @@ const renderEvent = (event: CalendarEvent) => { switch (event.type) { case "transaction": return renderLancamento(event); + case "installment": + return renderInstallment(event); case "boleto": return renderBoleto(event); case "card": @@ -185,6 +218,7 @@ export function EventModal({ open, day, onClose, onCreate }: EventModalProps) { const grouped = day ? { transaction: day.events.filter((e) => e.type === "transaction"), + installment: day.events.filter((e) => e.type === "installment"), boleto: day.events.filter((e) => e.type === "boleto"), card: day.events.filter((e) => e.type === "card"), } @@ -204,7 +238,7 @@ export function EventModal({ open, day, onClose, onCreate }: EventModalProps) {
{hasEvents && grouped ? ( - (["transaction", "boleto", "card"] as const) + (["transaction", "installment", "boleto", "card"] as const) .filter((type) => grouped[type].length > 0) .map((type) => (
diff --git a/src/features/calendar/queries.ts b/src/features/calendar/queries.ts index 5200f42..09297c8 100644 --- a/src/features/calendar/queries.ts +++ b/src/features/calendar/queries.ts @@ -136,6 +136,44 @@ export const fetchCalendarData = async ({ } } + // Agrupar parcelas da mesma série em um único evento + const installmentGroups = new Map< + string, + Array> + >(); + for (const event of events) { + if (event.type !== "transaction") continue; + const { seriesId, installmentCount } = event.transaction; + if (!seriesId || !installmentCount || installmentCount <= 1) continue; + const group = installmentGroups.get(seriesId) ?? []; + group.push(event as Extract); + installmentGroups.set(seriesId, group); + } + + const groupedSeriesIds = new Set(); + const installmentEvents: CalendarEvent[] = []; + for (const [seriesId, group] of installmentGroups) { + if (group.length < 2) continue; + groupedSeriesIds.add(seriesId); + const rep = group[0]; + installmentEvents.push({ + id: `${seriesId}:installment`, + type: "installment", + date: rep.date, + transaction: rep.transaction, + installmentCount: rep.transaction.installmentCount ?? group.length, + installmentValue: rep.transaction.amount ?? 0, + }); + } + + const baseEvents = events.filter((e) => { + if (e.type !== "transaction") return true; + const { seriesId } = e.transaction; + return !seriesId || !groupedSeriesIds.has(seriesId); + }); + + const allEvents = [...baseEvents, ...installmentEvents]; + // Vencimentos de cartões com lançamentos no período for (const card of cardRows) { if (!cardTotals.has(card.id)) continue; @@ -151,7 +189,7 @@ export const fetchCalendarData = async ({ const isPaid = paymentByCardName.has(card.name); const paymentDate = paymentByCardName.get(card.name) ?? null; - events.push({ + allEvents.push({ id: `${card.id}:cartao`, type: "card", date: dueDateKey, @@ -172,11 +210,12 @@ export const fetchCalendarData = async ({ const typePriority: Record = { transaction: 0, + installment: 0, boleto: 1, card: 2, }; - events.sort((a, b) => { + allEvents.sort((a, b) => { if (a.date === b.date) { return typePriority[a.type] - typePriority[b.type]; } @@ -192,7 +231,7 @@ export const fetchCalendarData = async ({ const estabelecimentos = await fetchRecentEstablishments(userId); return { - events, + events: allEvents, formOptions: { payerOptions: optionSets.payerOptions, splitPayerOptions: optionSets.splitPayerOptions, diff --git a/src/shared/lib/types/calendar.ts b/src/shared/lib/types/calendar.ts index e48191d..400797b 100644 --- a/src/shared/lib/types/calendar.ts +++ b/src/shared/lib/types/calendar.ts @@ -10,6 +10,14 @@ export type CalendarEvent = date: string; transaction: TransactionItem; } + | { + id: string; + type: "installment"; + date: string; + transaction: TransactionItem; + installmentCount: number; + installmentValue: number; + } | { id: string; type: "boleto";